Skip to content

Latest commit

 

History

478 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Codohue

Codohue is a hybrid (sparse + dense) collaborative-filtering recommendation service for behavioral personalization.

It ingests events and raw catalog content over HTTP and durable Redis Streams, persists them in PostgreSQL, recomputes sparse/dense vectors on a schedule, auto-embeds catalog content (the recommended core mode), and serves recommendations and candidate rankings through HTTP APIs backed by Qdrant.

Architecture details, data model, API surface, design decisions: see ARCHITECTURE.md. Contributing — commits, branches, pull requests: see Contributing. Instructions for AI coding agents (Codex, Claude Code): see AGENTS.md. Go SDK: see sdk/go/README.md.

Highlights

  • Ingest events via POST /v1/namespaces/{ns}/events or Redis Stream codohue:events
  • Sparse CF + dense (item2vec / svd / byoe / disabled) blended at serve time
  • Auto-embedding of raw catalog content per namespace (dense_source="catalog" — the core mode; item2vec/svd/byoe remain as options)
  • Time-decayed trending ZSET in Redis; 5-minute recommendation cache
  • Multi-tenant: each namespace owns its config, Qdrant collections, Redis streams, and API key
  • Operational SPA on port 2002 (session-cookie auth)

Binaries

Binary Port Purpose
cmd/api 2001 Data-plane HTTP API + Redis Streams ingest worker
cmd/cron — Batch daemon: sparse + dense + trending recompute
cmd/admin 2002 Admin server: /api/admin/v1/* (operator session cookie or scoped service token) + embedded SPA
cmd/embedder 2003 Catalog auto-embedding worker

Requirements

  • Go 1.26.7 (server). SDK modules (pkg/codohuetypes, sdk/go, sdk/go/redistream) target Go 1.24.13.
  • Docker + Docker Compose 2.24+ for local infra
  • golangci-lint (for make lint, make fmt)
  • air (for make dev)
  • migrate (for host-side make migrate-*)
  • Node.js 22.12+ and npm (for web/admin; Docker and CI use Node 22)

Quickstart — full stack with Docker Compose

cp .env.example .env
# Generate and protect the three secret files first; see deploy/operator-auth.md.
make up-d

This starts PostgreSQL, Redis, Qdrant, migrations, a trusted provisioning init service and the four app containers. Follow operator setup to generate the owner, proxy and application secret files once before starting.

Verify:

curl http://localhost:2001/healthz
open  http://localhost:2002       # admin SPA login (individual owner account)

Other compose layouts:

File Purpose
compose.yaml Dev — builds from source, infra included, auto-migrate
compose.app.yaml App-only — builds from source, host networking to external infra
compose.prod.yaml Prod — prebuilt GHCR images; each infra service is optional: run it in-compose via COMPOSE_PROFILES (local-db, local-redis, local-qdrant) or point at an external one via CODOHUE_DATABASE_URL / CODOHUE_REDIS_URL / CODOHUE_QDRANT_HOST

See Docker deployment for app-only setup, external database requirements, deployment verification, and upgrades. Local infrastructure and embedder ports bind to loopback. Containers use Compose project names; keep the project directory/name unchanged when upgrading to reuse existing volumes.

Quickstart — binaries against Docker infra

For fast Go iteration without rebuilding images:

make up-infra        # postgres + redis + qdrant only
make migrate-up      # apply migrations from host
make run             # cmd/api (or run-cron / run-admin / run-embedder)
make dev             # cmd/api with live reload (air)
make dev-all         # api (air) + admin + web/admin Vite together

Configuration

Codohue loads .env automatically when present. Required: DATABASE_URL (or DATABASE_URL_FILE). Human accounts and scoped service tokens replace the shared admin key; see setup, secret files and migration.

Variable Default Used by
REDIS_URL redis://localhost:6379 all
QDRANT_HOST / QDRANT_PORT localhost / 6334 all
CODOHUE_API_PORT 2001 cmd/api
CODOHUE_ADMIN_PORT 2002 cmd/admin
CODOHUE_API_URL http://localhost:2001 cmd/admin (proxy /healthz, inject events)
CODOHUE_BATCH_INTERVAL_MINUTES 5 cmd/cron
CODOHUE_LOG_FORMAT text all (text or json)
CODOHUE_CATALOG_MAX_CONTENT_BYTES 32768 cmd/api catalog ingest default (override per namespace via admin API)
CODOHUE_EMBED_MAX_ATTEMPTS 5 cmd/embedder retries before dead-letter
CODOHUE_EMBEDDER_HEALTH_PORT 2003 cmd/embedder
CODOHUE_EMBEDDER_REPLICA_NAME hostname cmd/embedder consumer name
CODOHUE_EMBEDDER_POLL_INTERVAL 30s cmd/embedder rescan cadence
CODOHUE_OBSERVABILITY_TOKEN unset cmd/api, cmd/admin, cmd/embedder — gates /metrics and API/embedder /healthz?details=true. Unset means those routes do not exist (404)
CODOHUE_STREAM_RETENTION_ENABLED false cmd/api, cmd/embedder — exact consumer-progress stream trimming
CODOHUE_STREAM_RETENTION_INTERVAL 1m how often a retention pass runs

Operational endpoints

/healthz is public and reports only an aggregate status — no component names, no dependency errors. Anything more detailed needs the dedicated observability credential, which is deliberately separate from any operator or service credential: a monitoring agent should not be able to delete namespaces.

curl http://localhost:2001/healthz                       # public: {"status":"ok"}
curl -H "Authorization: Bearer $CODOHUE_OBSERVABILITY_TOKEN" \
     "http://localhost:2001/healthz?details=true"        # per-component detail
curl -H "Authorization: Bearer $CODOHUE_OBSERVABILITY_TOKEN" \
     http://localhost:2001/metrics                       # Prometheus scrape

The embedder exposes the same pair on CODOHUE_EMBEDDER_HEALTH_PORT. The admin process exposes its own protected metrics at http://localhost:2002/metrics.

Namespace lifecycle generations

Every namespace carries a monotonic generation. Deleting and recreating a name mints a new one, and generation 2+ qualifies the namespace's physical artifacts (trending:ns:g2, ns_g2_objects_dense, catalog:embed:ns:g2), so work published against a deleted incarnation can never become visible to the new one. Generation 1 keeps the original unqualified names, so upgrading moves nothing.

Event, catalog and embed stream payloads carry an additive namespace_generation. Producers that predate it keep working during the adoption window — see deploy/backend-audit-remediation.md.

Failure contract

Data-plane writes answer distinguishably, so a client knows whether to retry:

Situation Status Code
Namespace absent or deleted 404 namespace_not_found
Namespace mid-delete, or a system reset in progress 409 namespace_not_active
Configuration or lifecycle store unreadable 503 namespace_config_unavailable
object_created_at more than five minutes in the future 400 invalid_object_created_at

Creating a namespace

The admin plane is the only place namespaces are configured. Login via the SPA, or with curl:

# 1. Create session cookie
curl -c cookies.txt -X POST http://localhost:2002/api/v1/auth/sessions \
  -H "Content-Type: application/json" \
  -H "X-Codohue-CSRF: 1" \
  -d '{"username":"owner","password":"<your-owner-password>"}'

# 2. Upsert namespace
curl -b cookies.txt -X PUT http://localhost:2002/api/admin/v1/namespaces/demo \
  -H "X-Codohue-CSRF: 1" \
  -H "Content-Type: application/json" \
  -d '{
    "action_weights": {"VIEW": 1, "LIKE": 5, "SHARE": 10},
    "lambda": 0.01, "gamma": 0.5,
    "max_results": 20, "seen_items_days": 14,
    "alpha": 0.7,
    "dense_source": "byoe", "embedding_dim": 4, "dense_distance": "cosine",
    "trending_window": 24, "trending_ttl": 600, "lambda_trending": 0.1
  }'

The response returns a plaintext namespace API key once — only the bcrypt hash is stored. Data-plane calls send it as Authorization: Bearer <key>; no credential bypasses that check by default. For repeatable Compose setup, supply a pre-generated provision_api_key or use admin access provision; matching retries preserve credentials/configuration and conflicts return 409.

Sending events

# HTTP
curl -X POST http://localhost:2001/v1/namespaces/demo/events \
  -H "Authorization: Bearer <namespace-key>" \
  -H "Content-Type: application/json" \
  -d '{"subject_id":"user-123","object_id":"item-456","action":"VIEW","occurred_at":"2026-04-19T10:00:00Z"}'

# Redis Streams (no URL → namespace lives in the payload)
redis-cli XADD codohue:events '*' payload \
  '{"namespace":"demo","subject_id":"user-123","object_id":"item-456","action":"VIEW","occurred_at":"2026-04-19T10:00:00Z"}'

Built-in actions: VIEW, LIKE, COMMENT, SHARE, SKIP. Custom actions are accepted when defined in namespace_configs.action_weights.

Testing

make test                                      # unit + package tests across all go.work modules
make test-race                                 # with -race
make test-pkg PKG=./internal/ingest/...        # one package tree
make up-infra && make migrate-up && make test-e2e

The E2E suite (build tag e2e) launches the API binary on port 12001 and exercises HTTP contracts, Redis Streams ingest, cron recompute, hybrid recommendation, and catalog auto-embedding.

Makefile is the source of truth for the rest — build, Docker, coverage, lint and migration targets.

Web admin SPA

The admin console at web/admin/ (Vite + React 19 + Tailwind v4) is embedded into cmd/admin at build time via the embedui build tag.

make dev-admin            # Vite dev server (standalone)
make build-admin-embed    # production admin binary with SPA

make build-admin (no -embed) builds an admin binary that serves only the API.

Contributing

Work on a branch off main, keep the change focused, and open a pull request.

Commits and branches

Both use Conventional Commits vocabulary.

  • Commit: type(scope): summary — imperative mood, concise, lowercase except proper nouns, API names and versions.
  • Branch: type/scope-summary — lowercase and hyphen-separated, e.g. feat/api-namespace-routes.
  • Types: feat, fix, refactor, test, docs, ci, chore.
  • Scopes: api, cron, admin, embedder, ingest, compute, recommend, nsconfig, auth, catalog, embedstrategy, idmap, qdrant, redis, postgres, metrics, e2e, docs, ci.

Pick one primary scope per commit and keep each commit to one logical change. Describe repository behavior, not Spec Kit phase labels or task IDs such as T012. Older styles such as scope: summary or numeric branch names such as 001-feature-name are not used for new work.

Before opening a pull request

make fmt && make lint
make test && make test-race
make up-infra && make migrate-up && make test-e2e   # if you touched API behavior,
                                                    # migrations, Redis/Qdrant, or cron

Then in the pull request description:

  • describe the behavior change and the modules it affects,
  • call out migration or configuration changes explicitly,
  • include a sample request and response for API changes,
  • note rollout steps if Redis, Qdrant, or cron behavior changes.

Code conventions

  • Keep feature logic in its domain package. The default shape is docs.go, types.go, repository.go, service.go, handler.go, plus matching tests.
  • Domain packages may import core, infra, and config — not each other.
  • Every package needs a docs.go holding the package doc comment and package declaration only.
  • Every service.go, repository.go, job.go, and worker.go needs a _test.go; types.go and docs.go do not.
  • Write code comments, doc comments, and TODOs in English.
  • Update ARCHITECTURE.md whenever an endpoint, migration, storage contract, process responsibility, or cross-domain flow changes.
  • pkg/codohuetypes is the public wire contract; a deliberate change there must update its golden snapshots and the REST API table in ARCHITECTURE.md.
  • Never commit secrets or plaintext namespace keys.

Notes

  • make down-v removes containers and volumes — full local reset.
  • Catalog ingest (POST /v1/namespaces/{ns}/catalog) only works when the namespace uses dense_source="catalog", configured through PUT /api/admin/v1/namespaces/{ns}/catalog; in that mode, PUT /v1/namespaces/{ns}/objects/{id}/embedding returns 409 Conflict because the catalog pipeline owns object vectors.

About

Hybrid recommendation service for behavioral personalization. Codohue ingests events from Redis Streams, stores raw data in PostgreSQL, computes sparse and dense vectors, and serves namespace-scoped recommendations through an HTTP API backed by Qdrant.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages