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#
| Service | What | Port | Version | Health |
|---|---|---|---|---|
| Engine | one Go binary: gateway, ingest, catalog, serve, proof, admin, webhooks | 5300 | d27401b | GET https://patterna-api.orven.am/v1/health |
| Jobs worker | Python 3.12: learning, enrichment, connectors, deletions, retention, cost ledger, alerts, reports | 5320 | see the jobs worker /healthz | GET /healthz on the worker |
| Panel | SvelteKit control panel and these docs | 5310 | see the panel /healthz | GET /healthz on the panel |
Check the engine yourself:
curl -sS https://patterna-api.orven.am/v1/healthAddresses#
| What | Address |
|---|---|
| API | https://patterna-api.orven.am/v1 |
| Docs and panel | https://patterna.orven.am |
| Contract | https://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#
| Store | Status |
|---|---|
| Postgres 17 + pgvector | required; holds everything, with row-level security per tenant. The precomputed candidate tables (user shortlists, item neighbours) live here too. |
| Redis | optional 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 bucket | not built; exports land in the engine's export directory. |
Schedule#
The jobs worker runs these entries for every tenant. Times are UTC.
| Entry | When | Does |
|---|---|---|
partitions | daily 01:00 | creates monthly partitions for this month and the next three |
deletions | hourly | processes deletion requests that arrived as user_deleted events. DELETE /users/{id} does not wait for this: it deletes at once. |
connectors | every 15 min | runs each tenant connector whose own interval has passed |
enrich | hourly | tags and embeds pending items |
cost_ledger | daily 00:15 | rolls up LLM spend; warns at 80% of the daily ceiling |
learn_nightly | daily 02:00 | in 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 |
retention | daily 03:00 | removes data past its retention period per data class |
ledger_monthly | 1st of the month, 04:00 | writes the ledger snapshot for the previous month |
twin_weekly | daily 05:00 | refreshes the twin curve per surface |
analyst_notes | weekly | writes the analyst note on the overview |
workspace_deletion | hourly | deletes a workspace whose seven-day notice has passed |
alerts | every 5 min | sends alert notifications as email and Slack messages |
lab_runs, lab_ai | every 2 min | runs 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. Reasonsimilar.params.k1 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_tierandtenure_days, next tohour_bucket,platform,regionandsession_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_fitandformat_fit, silent in reasons. filters.relevance_floorfor anchored surfaces, judged the same in both arms.- Truthful reasons and the reason key
recommended;pairs_withonly for a recipe or workout with a product, or a community with a document. curatedas a boost by rank.gbdtreads 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.regionwith tenant-configured regions andfilters.region_boost;context.session_itemsand thesession_itemsgenerator;ranker.banditwith 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_fitandformat_fit. - Rankers
linear,learned_linearandgbdt.lambdamartis refused. - Plackett-Luce exploration with exact propensities, caps and
filters.diversityinside the sampler. Exploration runs on treatment slates and on slates outside any holdout (armnone); control is never explored. - Holdouts with sticky arms,
stratify_by: "persona"(defaultnone) with optionalstrata_shares, the ledger with clustered intervals and SRM, weekly rows, per-block rows (by=block), power, export, ending an experiment. A row isreadablewhen each arm reaches the experiment'sreadable_sample. - Anchored control: control serves the request's
context.control_item_ids, else the surface'sfallback.by_anchorlist for the anchor, else the surface's list, under the request's rules; the answer'scontrol_source(request,by_anchor,surface) says which. shown: falseanswers carry no items, count in their arm as served with zero success (intention to treat) and cannot be abandoned (409 not_shown).min_slotsis counted before the arm.- Fallback fill: a short treatment slate is filled from the editorial list;
fill_counton the answer and the impression,fill: trueand propensity 1 on each filled slot in the trace. model_versionson every impression, in trace and export.- Learning from served slates: the slate learners train on arms
treatmentandnone(never control) from the interactions you report, joined to the slate byrequest_id, plus signeditem_completedevents for served items. context.member_of: communities the user already joined are left out, in every mode.- A client deadline (
X-Deadline-Msordeadline_ms) and a defaultSERVE_BUDGETof 120 ms. See Timeouts and the ledger. POST /recommendations/{request_id}/abandon(client_abandoned), with a client-mintedX-Request-Id.assignment_unit(64 hex characters) for one arm per person across sessions. See Identity across connectors.X-Patterna-Tester: 1for testers from your own roster.context.measuredon the request;min_slots(withshown: false) anddevice_resorton the surface.- Interactions checked against the slate (
422 unknown_item), kind and position from the logged slate, an optionalblock. - Metrics
tap_rate,second_item_rate,join_rate(community surfaces),engaged_after_tapandpurchase_rate(product surfaces); surface metricsfallback_rateincluding abandoned slates,client_abandoned_rateandunmeasured_share. - Reasons: templates keyed by reason key or generator kind, placeholders
{anchor_title},{tag}and{segment}, reason keyspairs_with,more_about_tagandrecommended,400 invalid_reason_template.phase_fitin slates only when the tenant turnsphase_tagson (off by default). - Catalog upsert, status, tags,
localesper item, achanged_sincelisting andDELETE /catalog/items/{id}. - Events judged one by one, with an allowlist per type; PII field names rejected per event (
pii_rejectedinrejected). 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}/modenon_profiledorconsent_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 asnon_profiledwithout the id (audited asserve.optout_downgrade). PUT /users/{id}/profilewithoutmodefor an unknown user:422 mode_required.- Events
coach_product_shownandcoach_product_tappedwithproduct_id,conversation_idandposition. - 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
offrefused with409 surface_off. - Prometheus metrics at
/metrics, a load-test harness for the p99 budget. Unknown routes answer404JSON withcode: not_found. engine quickstart: runs the migrations, then creates ademotenant with 40 example items, a livehomesurface 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 andmember_of,shown, fill and control source, coach product events, an admin package and apatternaCLI withsurface apply, typed fields forweekday,subscription_tierandtenure_days, the catalog attributes and the new events, and (0.4.1)GetItemwithErrItemNotFound. Install: npm[email protected]; Gogo get github.com/payam-h/patterna-sdk/[email protected](taggo/v0.4.1); Python from tagv0.4.1ofpayam-h/patterna-sdk(subdirectorypython; 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 eventsjoin_rateneeds, so a ledger row cannot yet be recomputed from the export alone. - No revenue figure:
revenue_centsis not computed. difficulty,author,app_versionandtenure_daysare stored but not ranked on yet.- When a request has no
weekday,time_fituses 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-sdkis not on PyPI, by decision: install it from tagv0.4.1ofpayam-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.