Patterna Docs

Status

What runs where, which versions, what is built and what is not built.

Live engine health

Operational

Engine
d27401b
Postgres
ok
Redis
ok
GET /health
3 ms

Checked 19:32:40

What runs where today. Version numbers come from the running services.

Services#

ServiceWhatPortVersionHealth
Engineone Go binary: gateway, ingest, catalog, serve, proof, admin, webhooks5300d27401bGET https://patterna-api.orven.am/v1/health
Jobs workerPython 3.12: learning, enrichment, connectors, deletions, retention, cost ledger, alerts, reports5320see the jobs worker /healthzGET /healthz on the worker
PanelSvelteKit control panel and these docs5310see the panel /healthzGET /healthz on the panel

Check the engine yourself:

curl
curl -sS https://patterna-api.orven.am/v1/health

Addresses#

WhatAddress
APIhttps://patterna-api.orven.am/v1
Docs and panelhttps://patterna.orven.am
Contracthttps://patterna.orven.am/docs/openapi.yaml

These are the only public addresses. Keys for a separate staging environment are issued per customer with the environment's own address.

Self-hosting artefacts exist: container images for the engine, panel and jobs, a Docker Compose file for one host and a Helm chart for Kubernetes, with the runbook section and a measured capacity baseline. Ask for them if Patterna must run on your own infrastructure.

Stores#

StoreStatus
Postgres 17 + pgvectorrequired; holds everything, with row-level security per tenant. The precomputed candidate tables (user shortlists, item neighbours) live here too.
Redisoptional shared cache. Without REDIS_URL each engine instance keeps its own in-process cache (surfaces 60 s, aggregate answers 45 s). With it, the 45 s aggregate answers (ledger, surface metrics, users summary) are shared across instances. Nothing else needs Redis.
S3-compatible bucketnot built; exports land in the engine's export directory.

Schedule#

The jobs worker runs these entries for every tenant. Times are UTC.

EntryWhenDoes
partitionsdaily 01:00creates monthly partitions for this month and the next three
deletionshourlyprocesses deletion requests that arrived as user_deleted events. DELETE /users/{id} does not wait for this: it deletes at once.
connectorsevery 15 minruns each tenant connector whose own interval has passed
enrichhourlytags and embeds pending items
cost_ledgerdaily 00:15rolls up LLM spend; warns at 80% of the daily ceiling
learn_nightlydaily 02:00in order: embeddings, the user model and item similarity (with co-purchase pairs), tag affinity and personas (with format preferences and purchases), item signals (time profiles and item features), then per surface in testers or live the persona model with SNIPS, the learned linear ranker, the gbdt ranker and the bandit posteriors, then the month's ledger snapshot and generator attribution. A step whose inputs did not change is skipped (for at most 7 days); marks deletions retrained
retentiondaily 03:00removes data past its retention period per data class
ledger_monthly1st of the month, 04:00writes the ledger snapshot for the previous month
twin_weeklydaily 05:00refreshes the twin curve per surface
analyst_notesweeklywrites the analyst note on the overview
workspace_deletionhourlydeletes a workspace whose seven-day notice has passed
alertsevery 5 minsends alert notifications as email and Slack messages
lab_runs, lab_aievery 2 minruns queued simulations and AI proposals

Built#

Everything below runs on engine 0.3.3. engine_version on /health reports that number (it is also the contract version), next to the build in release. The current SDKs are 0.4.1.

Retrieval at scale and real-time sessions (engine 0.3.3)#

  • vector_retrieval: a generator that is also a candidate source. Per request the engine builds one query vector from what the person looked at (the session's items in every mode, the recent positives when personalized) and reads the k nearest items by content from the vector index over the catalog's embeddings; they join the candidates with their cosine as the score. Reason similar. params.k 1 to 500, default 100.
  • Real-time session state: with Redis configured the session's reports and signed events are kept for two hours with the session's running content vector, and a slate requested right after a tap reflects it. Without Redis, or when Redis is down, the session is read from the log as before. Nothing new to send.
  • Numbers (100,000 items, 50,000 users, one engine instance, offline MovieLens replay): the repository's docs/LOADTEST.md and docs/GORSE_COMPARISON.md, "Round 8".

The signals (engine 0.3.1)#

  • The request context weekday, subscription_tier and tenure_days, next to hour_bucket, platform, region and session_items; logged with the impression, never on the user. See What the engine uses, and what it refuses.
  • Catalog attributes read by the engine: format, duration_min/reading_time_min, difficulty, author, available/in_stock, expires_at, starts_at, repurchase_days.
  • Hard rules in both arms: expired items, past events and unavailable products are never served (filters.hide_unavailable).
  • Personal exclusions, relaxed when nothing else is left: exclude_dismissed_days (30), exclude_consumed_days (30), paywall_cooldown_days (7), repurchase_days (30).
  • Generators time_fit and format_fit, silent in reasons.
  • filters.relevance_floor for anchored surfaces, judged the same in both arms.
  • Truthful reasons and the reason key recommended; pairs_with only for a recipe or workout with a product, or a community with a document.
  • curated as a boost by rank.
  • gbdt reads the item features and the request's hour, weekend and platform.
  • Events community_post_created, community_reply, community_reaction.
  • Jobs: the Shopify storefront sync (public products.json, no token), on the connector schedule per tenant (shopify_storefront, every_minutes) or on request, and the orders export reader (co-purchase pairs, no customer field).

Context, regions and the quota#

  • context.region with tenant-configured regions and filters.region_boost; context.session_items and the session_items generator; ranker.bandit with logged weights; engagement-weighted trending; no exploration on unmeasured slates; a monthly request quota (429 quota_exceeded).

Earlier#

  • Serve in three consent modes, with generators trending (per surface, all locales together), curated, item_to_item, content_affinity, persona_bayes, user_bayes, user_to_user, surface_specific, session_items, time_fit and format_fit.
  • Rankers linear, learned_linear and gbdt. lambdamart is refused.
  • Plackett-Luce exploration with exact propensities, caps and filters.diversity inside the sampler. Exploration runs on treatment slates and on slates outside any holdout (arm none); control is never explored.
  • Holdouts with sticky arms, stratify_by: "persona" (default none) with optional strata_shares, the ledger with clustered intervals and SRM, weekly rows, per-block rows (by=block), power, export, ending an experiment. A row is readable when each arm reaches the experiment's readable_sample.
  • Anchored control: control serves the request's context.control_item_ids, else the surface's fallback.by_anchor list for the anchor, else the surface's list, under the request's rules; the answer's control_source (request, by_anchor, surface) says which.
  • shown: false answers carry no items, count in their arm as served with zero success (intention to treat) and cannot be abandoned (409 not_shown). min_slots is counted before the arm.
  • Fallback fill: a short treatment slate is filled from the editorial list; fill_count on the answer and the impression, fill: true and propensity 1 on each filled slot in the trace.
  • model_versions on every impression, in trace and export.
  • Learning from served slates: the slate learners train on arms treatment and none (never control) from the interactions you report, joined to the slate by request_id, plus signed item_completed events for served items.
  • context.member_of: communities the user already joined are left out, in every mode.
  • A client deadline (X-Deadline-Ms or deadline_ms) and a default SERVE_BUDGET of 120 ms. See Timeouts and the ledger.
  • POST /recommendations/{request_id}/abandon (client_abandoned), with a client-minted X-Request-Id.
  • assignment_unit (64 hex characters) for one arm per person across sessions. See Identity across connectors.
  • X-Patterna-Tester: 1 for testers from your own roster.
  • context.measured on the request; min_slots (with shown: false) and device_resort on the surface.
  • Interactions checked against the slate (422 unknown_item), kind and position from the logged slate, an optional block.
  • Metrics tap_rate, second_item_rate, join_rate (community surfaces), engaged_after_tap and purchase_rate (product surfaces); surface metrics fallback_rate including abandoned slates, client_abandoned_rate and unmeasured_share.
  • Reasons: templates keyed by reason key or generator kind, placeholders {anchor_title}, {tag} and {segment}, reason keys pairs_with, more_about_tag and recommended, 400 invalid_reason_template. phase_fit in slates only when the tenant turns phase_tags on (off by default).
  • Catalog upsert, status, tags, locales per item, a changed_since listing and DELETE /catalog/items/{id}.
  • Events judged one by one, with an allowlist per type; PII field names rejected per event (pii_rejected in rejected). No raw payload is stored.
  • Webhooks: PostHog with X-Patterna-Token, Shopify at /webhooks/shopify/{tenant} with HMAC only.
  • Immediate deletion with DELETE /users/{id}; export and trace per user. Opt-out (PUT /users/{id}/mode non_profiled or consent_changed) deletes the user row in the request and keeps only a salted-hash marker; the hourly job unlinks impressions (interactions stay) and deletes events, assignments and the shortlist. Profiled requests for an opted-out id are served as non_profiled without the id (audited as serve.optout_downgrade).
  • PUT /users/{id}/profile without mode for an unknown user: 422 mode_required.
  • Events coach_product_shown and coach_product_tapped with product_id, conversation_id and position.
  • Scoped keys (pk_s_, pk_i_, pk_a_), rate limits, signing secrets (pss_…, used as the string; create and rotate), MFA, roles, two-person rule, audit log.
  • Surface status off refused with 409 surface_off.
  • Prometheus metrics at /metrics, a load-test harness for the p99 budget. Unknown routes answer 404 JSON with code: not_found.
  • engine quickstart: runs the migrations, then creates a demo tenant with 40 example items, a live home surface and one key per scope, and prints the first three calls.
  • SDKs 0.4.1 (Go, TypeScript, Python): Abandon, RecommendMany, a tester option, the deadline, control lists and member_of, shown, fill and control source, coach product events, an admin package and a patterna CLI with surface apply, typed fields for weekday, subscription_tier and tenure_days, the catalog attributes and the new events, and (0.4.1) GetItem with ErrItemNotFound. Install: npm [email protected]; Go go get github.com/payam-h/patterna-sdk/[email protected] (tag go/v0.4.1); Python from tag v0.4.1 of payam-h/patterna-sdk (subdirectory python; not on PyPI, by decision).

Not built#

Engine:

  • S3 export target.
  • A shared Redis cache for anything other than the aggregate answers.
  • The proof export carries impressions and interactions only. It lacks the clustering unit, measured, not_shown, tester, fallback, blocks, the experiment window and the events join_rate needs, so a ledger row cannot yet be recomputed from the export alone.
  • No revenue figure: revenue_cents is not computed.
  • difficulty, author, app_version and tenure_days are stored but not ranked on yet.
  • When a request has no weekday, time_fit uses the engine's clock in UTC, not your timezone.

Jobs:

  • Parquet PostHog exports and the GA4 intraday table.
  • The event weights are fixed in the learner, not a per-tenant setting.

SDKs:

  • Python patterna-sdk is not on PyPI, by decision: install it from tag v0.4.1 of payam-h/patterna-sdk (pip install "patterna-sdk @ git+https://github.com/payam-h/[email protected]#subdirectory=python"). npm ([email protected]) and Go (go/v0.4.1) are published.

See the Changelog for what changed and when.

esc
  • Type to search every page, heading and code sample.