Arya architecture (as built)
Status: living document, revised 2026-11-04. Section 0 is the one-page map with the diagram. Sections 1 to 13 are the design record. SPEC.md states the product rules. This document states how the built system is put together. Where the two disagree, SPEC.md wins for behavior and this document wins for structure.
0. The whole app on one page
Detailed view (wide, scrolls sideways on a phone):
Phone-friendly view of the same map
/api/mcpbearer token, stored hashed/api/v1bearer token/ and the APIapi/routes/Code · $0
Meta models · via meta_client.py
arya.user_docs, in dual or pg modeThe diagram is assistant/docs/architecture.svg (plain SVG, light and dark),
redrawn and checked against main on 2026-11-04. The sections below say what
each box does. Their facts are checked against main on 2026-11-04, except the
live-site values in §0.11 and §10, which come from the Ops screen.
In one paragraph. A phone-first PWA (React, built with Vite) talks
to one FastAPI process, which also serves the built app at /. Caddy fronts it with TLS. Code does the
money, timing and list logic at $0. Meta models read photos, judge
staples and answer questions, and every call to them goes through one
module (api/meta_client.py). State is one JSON document per area, kept
on a Docker volume, in Supabase Postgres, or both.
0.1 Front end (assistant/web/)
- React 19 and TypeScript, built by Vite into
web/dist. The API serves the build at/and the static folderassistant/pwa/(images, store models, three.js,scene.js, the manifest, icons) beside it; the build wins on a name clash.index.htmlandsw.jsare sent withCache-Control: no-cache. The old address/next/...answers 302 to the same path at/. - One screen per file in
web/src/screens/, listed inscreens/registry.tsx. Navigation is a stack (app/nav.tsx); the session (user, demo, currency, language) isapp/session.tsx. Shared parts:web/src/ui/(sheet, toast, info sheet, the store view) andweb/src/lib/(API calls, formats, stores, language). - The hunt, Express, Pack, haul, album, share, mapper, group live sync
(
app/groupLive.ts), push reminders, powers and the install prompt are ported one to one from the old plain-JS files. The store view isscene.js(three.js), loaded as a classic script. - The navigation tab is Today, Spending, Quest, Ask and More
(the default; it can be customized). More holds Lists, Receipts, Stores, Help
and Settings. Settings opens the Profile page (Group, Preferences with
Reminders, Account, About Arya, Sign out last). About Arya is the
aboutview: Arya's powers, Use Arya in Muse, Add to Home Screen, Keeping Arya running. (Updated 2026-11-02, #416; this replaced the earlier six-tab layout.) - Service worker
web/src/sw.js, built to/sw.js(vite-plugin-pwa, injectManifest). It precaches the built shell and the runtime files of the static folder. It is network first. It sends/api/,/auth/,/decode,/importand/eyesstraight to the network, never touches a write (non-GET) or the live stream/api/group/live, and shows push reminders. It takes control at once and deletes every other cache, the oldarya-v*caches too. - Security: no inline script (the CSP has
script-src 'self';tests/test_csp.pychecksweb/index.html); no inline handlers. - Tests: string pins in
assistant/tests/(they readweb/srcthroughtests/frontend.py), vitest tests beside the code (web/src/**/*.test.ts*), and browser runs inweb/e2e/. - Style: light theme, greyscale structure and one orange accent; see
DESIGN.md.
0.2 API (assistant/api/)
api/main.py is a short file: the app, the require_login middleware,
a handler that turns a model failure (RuntimeError) into a 502 with a
plain sentence, /health, one ordered router list, the Lists API mounted
at /api/v1, and the static mount for the PWA. Routes live in api/routes/, one router per area:
| Router | What it serves |
|---|---|
auth, account | sign-in, registration (closed unless ARYA_REGISTRATION_OPEN=1; the Ops flag overrides the env, and sign-up is closed on the live site), email code (Supabase) and Google (Arya's own sign-in, flagged), connector and Lists API token, settings, export, feedback, push opt-in |
intake, review, receiptjobs | receipt photo, PDF, text, order text, structured JSON, Costco JSON, voice transcribe, Curator, review queue; receipt batches that keep reading while the app is closed |
lists, items, expiry | named lists, add by text or photo, check, move, judge runs, item pages and flags, expiry dates |
home, chat | the Today payload, Ask Arya and its apply step |
prices | own-receipt prices, Analysis insights, Arya's powers |
groups | shared home: create, invite, join, leave, live stream |
quest, questmap, receipts, shelf, storemap, stores, eyes, avatar, friends | hunts, filing items into a store's zones, receipts with originals, shelf scan, Map my store, store directory, pantry and shelf photos kept as sightings, avatar, friends |
mcp_connector | /api/mcp, the tool surface for a Muse agent |
lists_v1 (mounted at /api/v1) | the Open Lists API: read and write your own lists from another app, bearer token only, its own error format |
ops | the owner's Ops screen: models, limits, accounts, freezes, Ask locks (owner only) |
town | Demo Town routes; the demo account only, and only when ARYA_DEMO_TOWN=1 |
Older siblings stay beside the package: album_routes, hunt_routes,
haul_routes, pack. Shared plumbing is in api/deps.py (cost ledger,
budget guard, new_client(), rate-limit buckets, JSON helpers),
api/demo_guard.py (login middleware, demo sandbox, door caps, the list of
routes a token cannot reach), api/ask_guard.py (the Ask input and output
guard and its three-strike lock, §6) and api/settings.py with
api/site_flags.py (a value set in Ops, then the env var, then the default).
0.3 Engines in code ($0)
decode/service.py (PDF text layer, scanned PDF pages rendered with
pypdfium2 and sent to the photo reader, arithmetic reconcile),
ledger/ (returns netted FIFO, allocations), patterns/ (cadence and
price by store), list/ (the derived list and its reasons),
state.running_low (one rule for Today and Ask), prices.py (see §0.7),
expiry.py, dictionary/, optimizer.py (merges duplicate adds and fixes typos before a hunt) and cart/ (a sandbox cart proposal; approval is a state change only, and the app no longer shows it).
0.4 Models
All calls go through api/meta_client.py; the personal-data rule is
enforced there. open_client.py is the only other model client.
| Job | Model | Notes |
|---|---|---|
| Reasoning, decode fallback, judge, Curator, Eyes, Ask Arya | muse-spark-1.3 (Standard; ARYA_SPARK_TIER=contributor is an operator option, off) | cached prefixes, low effort |
Receipt photo reader (default, auto) | Spark first; muse-wonder-1.0 (90 s bound) re-reads only a receipt the arithmetic checks flag | decoded_by records which read was kept; ARYA_PHOTO_READER=spark or wonder, or the Ops "Photo reader" switch, changes it with no code change |
| Celebration characters, generated once | muse-image-1.0 | |
| Voice in | muse-voice-transcribe-1.0 | |
| Shelf and people segmentation | sam-3.1 | people SAM detects are pixelated first, and the frame is discarded if that fails (detection is not perfect and has not been scored) |
| Second judge seat, dictionary enrichment | Spark Open, hosted on Baseten | platform data only; the client refuses personal data |
0.5 Storage
api/state.py keeps one JSON document per former file (receipts,
dictionary, flags, lists, quest, push...). ARYA_STORE picks the
backend: json (default; files under data/users/<account>/),
dual (files are the truth, each save mirrors to Postgres) or pg
(Supabase table arya.user_docs, keyed by owner and document name,
role arya_app, via DATABASE_URL). The demo account always uses files,
and its visitors write only to an in-memory sandbox overlay. Caches and
big blobs (shelf_matches.json, storemap_img.json) stay on disk in
every mode. A receipt's original photo or PDF is kept per account.
0.6 Auth
Password accounts (PBKDF2 hashes, throttled sign-in, cookie sessions
whose tokens are stored as SHA-256 hashes). Account names allow only
letters, digits and . _ % + - @. Email-code sign-in goes through Supabase
Auth and Google sign-in is Arya's own (api/google_oauth.py, used when
GOOGLE_OAUTH_CLIENT_ID and GOOGLE_OAUTH_CLIENT_SECRET are set; otherwise
Google goes through Supabase). Either way the provider only proves an email
address; Arya keeps its own account and cookie (api/supabase_auth.py, flag
ARYA_SUPABASE_AUTH). The Muse connector and the Lists API use a bearer
token whose hash is stored.
0.7 Price intelligence
api/prices.py is deterministic and uses only the household's own
receipts: per-store unit prices, a cheaper-store hint when the gap is
real, a trend only with enough buys over enough days, the household's own
basket inflation, a split-the-list suggestion, food waste in dollars.
Nothing is fetched from a store, a flyer, an API or the web. Ask Arya
answers price questions from these precomputed facts. Community prices,
Deals and Arya Shoppers are opt-in, recorded only, and not live.
0.8 Groups and live sync
With ARYA_GROUPS=1 (on in compose), an account in a group reads and
writes the shared stores (lists, flags, product knowledge) in the group's
plane; receipts stay with the account that filed them and every member
sees them. Invites are a 6-letter code valid 48 hours. Leaving copies
the lists. Live sync: every shared save calls live.changed(group_id);
each open app holds one Server-Sent Events stream (/api/group/live)
and reloads when its group changes. With DATABASE_URL set the change is
a Postgres NOTIFY arya_group and one listener thread per process wakes
its streams; without it, or with ARYA_LIVE_PG=0, the wake stays
in-process.
0.9 Push reminders
Flag ARYA_PUSH_REMINDERS=1 plus VAPID keys. A phone opts in from
Settings; the subscription is saved in push.json. A daily cron line
runs python -m api.reminders, which sends at most one push per account
per day for staples that run out inside the notice window. No model
calls. Steps: PUSH-REMINDERS.md.
0.10 Deploy
One Oracle VM, Docker Compose with two services: api (uvicorn,
deploy/Dockerfile.api) and caddy (TLS, security headers, CSP, plain
HTTP redirects to HTTPS). Data lives in the arya-data volume. Settings
come from deploy/.env (VM only); compose lists every variable the app
reads, and tests/test_compose_env.py fails if one is missing. Deploy is
git pull then docker compose up -d --build in assistant/deploy
(deploy.sh); never rsync --delete. The api image builds the app
(web/) in a node stage and copies web/dist in. backup.sh and restore.sh
handle the nightly backup. CI is manual only
(.github/workflows/tests.yml).
0.11 Feature flags and settings
Most limits and switches can also be set in the owner's Ops screen (Settings,
then Ops and health). A value set in Ops wins over the env var, which wins over
the code default. The table gives the default in compose. The live site (checked with Ops on
Nov 2) differs in these places: ARYA_STORE=dual; push reminders on (daily
cron at 13:00 UTC); Supabase Auth and email sign-in on; Demo Town on; the
Kroger aisle lookup on; own-key sign-up off; photo reader auto; Spark tier
standard; new sign-up closed (the Ops flag wins over the env). Budgets: $10 a
day for the app, $100 a day for the demo (code default $2), $2 per demo visit (code default $0.40).
Per-account limits are $3 heads-up, $20 monthly pause and $10 hourly freeze.
| Variable | Default in compose | Effect |
|---|---|---|
ARYA_STORE | json | json, dual or pg storage |
DATABASE_URL | empty | Supabase Postgres connection (role arya_app) |
ARYA_GROUPS | 1 | shared households (code default is off) |
ARYA_LIVE_PG | 1 | live sync through Postgres NOTIFY when DATABASE_URL is set |
ARYA_PUSH_REMINDERS | 0 | push reminders; needs ARYA_VAPID_* keys |
ARYA_SUPABASE_AUTH | 0 | email code and Google sign-in; needs SUPABASE_URL, SUPABASE_ANON_KEY |
ARYA_SUPABASE_SIGNUP | 1 | 0 limits Supabase sign-in to existing accounts |
ARYA_REGISTRATION_OPEN | 0 | open password sign-up |
ARYA_PHOTO_READER | auto | receipt photo reader: auto (Spark first, Wonder on a flagged read), spark or wonder |
ARYA_DAILY_BUDGET_USD | 10 | app model-spend ceiling per day |
ARYA_USER_HEAVY_MONTHLY_USD | 3 | one account's month of AI spend that shows a heads-up on its usage screen |
ARYA_USER_MONTHLY_CEILING_USD | 5 | one account's month of AI spend that pauses its paid reads until the 1st |
ARYA_USER_HOURLY_FREEZE_USD | 1 | one account's hour of AI spend that switches its paid reads off for ARYA_USER_FREEZE_HOURS (24); I can lift it with POST /api/usage/unfreeze |
ARYA_COST_PUBLIC_MIN | 50 | accounts with spend before the whole-Arya totals show on the usage screen |
ARYA_COST_PAID_BY | Hackathon credits | who the usage screen says pays for the AI reading |
ARYA_DEMO_DAILY_BUDGET_USD | 100 (code default 2) | demo household's day bucket |
ARYA_DEMO_SESSION_BUDGET_USD | 2.00 (code default 0.40) | one demo visit's share |
ARYA_DEMO_DOOR_RATE, ARYA_DEMO_AUTH_RATE | 240, 120 | per-IP demo door and auth rate limits |
ARYA_FEEDBACK_DAILY_CAP | 5 | feedback messages per account per day |
ARYA_TOKEN_GUARDS | on | an API token cannot call the blocked routes (bulk model spend, groups, push, feedback, account export, Kroger aisle hints); off lifts the list |
ARYA_DEMO_TOWN | 0 | demo household shops at simulated stores in a simulated neighborhood (DEMO-TOWN.md) |
ARYA_OWNER_USERNAME | empty | the owner account: it runs Ops and health (models, limits, accounts, freezes, Ask locks, registration). The owner can hand the role to another account in Ops; the old owner keeps owner rights for 24 hours so a wrong pick can be undone |
ARYA_OPS_EMAIL, ARYA_ALERT_EMAIL, RESEND_API_KEY | empty | email through Resend: delete-account codes, waitlist and feedback notes, and one alert per Ask lock (at most 20 a day); off until the key is set |
GOOGLE_OAUTH_CLIENT_ID, GOOGLE_OAUTH_CLIENT_SECRET | empty | Arya's own Google sign-in; without both, Google goes through Supabase |
ARYA_SCAN_DAILY_FRAMES | 120 | Map my store frames per account per day |
ARYA_KROGER_AISLES, KROGER_CLIENT_ID, KROGER_CLIENT_SECRET, ARYA_KROGER_DAILY_CALLS | 0, empty, empty, 800 | live Kroger aisle hint (api/kroger.py); never stored |
MODEL_API_KEY, BASETEN_API_KEY, ARYA_OPEN_MODEL, GOOGLE_PLACES_API_KEY | empty | model keys and the store picker |
0.12 Cost guards
Every model call writes a row to the cost ledger. The app, the demo
household and each demo visit spend from separate buckets (§10). Live
doors, batch judge runs and hunt creation pass the guard first; the
demo's live doors also have per-IP caps. Deterministic code never calls a
model. An Ask message the guard refuses costs $0 (no model call), and
max_tokens bounds each Ask reply (§6).
1. The shape: two layers
Arya has two layers. Each layer does the work it is good at.
- The edge is the user's own Muse. It sees the media: receipt photos, PDFs, pasted orders. It reads them and sends Arya structured data only; on this path no receipt image reaches the server. A receipt photo filed in the app itself is kept per account and is viewable in the app ("View receipt", Discussion 42). This is an accepted trade-off, recorded in SPEC.md.
- The core is the Arya server. It verifies, remembers, reasons, and answers. It spends no tokens on reading media.
The rule between the layers: the client proposes, Arya disposes. Client data is untrusted. Totals must reconcile in code before a receipt is accepted. Low-confidence lines go to the review queue.
2. Intake paths
All paths end in one contract: a decoded receipt with lines, subtotal, tax, and total.
- Structured intake (primary). POST /api/intake/structured. The user's Muse posts the decoded receipt as JSON. The server recomputes every line status. A subtotal that does not match the lines fails reconciliation and is marked, never silently accepted.
- Order text. POST /api/intake/order-text. A pasted online order (Instacart, Amazon) is extracted server-side, then follows the same path. Delivery fees and tips are excluded by rule.
- Server decode (fallback). PDF text and photo decode endpoints remain for clients that cannot decode at the edge. They use the Model API under the model policy (MODEL-POLICY.md).
- Costco JSON export. Bootstrap seed only. It is not a product intake path.
3. Verification in code (no model, no cost)
- Arithmetic: line totals must sum to the subtotal within 5 cents.
- Line status: auto at confidence 0.8 or above, else unverified and into the review queue.
- Discount lines are structure, not identity. A discount line
(Costco prints it as "/
- ") attaches to its parent item (named on the receipt, else the previous item line) and is always auto-verified. It never enters the review queue, on any path.
- Fee lines are classified once and never enter patterns or the list.
4. Identity: what things are
Three mechanisms, in order of strength.
- The dictionary maps each store SKU to a canonical name. A name the user confirms teaches the dictionary. An item whose name cannot be established is unresolved: off the list, out of patterns, waiting in the review queue. Arya never guesses a name.
- Groups. Products are grouped by an explicit group key when one is set, else by their normalized name. Costco re-SKUs the same product under one name; the group is the product everywhere it matters: patterns, ledger, list. Merges and splits set the explicit key.
- The Curator (server-side reasoning) proposes merges and splits inside candidate clusters built in code at no cost. The model judges a cluster under a strict doctrine: same product only; never across organic vs conventional, form, flavor, fat content, or a size that makes a different product; when in doubt, keep separate. A proposal changes nothing until the user applies it. No silent edits, ever. Proposals the dictionary has already overtaken (members already one group, a duplicate, a split already in effect) retire automatically as superseded and are never shown.
5. The Resolver: research at the edge
Arya's server models have no web access. Identification from the outside world is therefore an edge job, run by the user's Muse:
- Trigger: an unresolved dictionary item, or an unverified receipt line whose name is abbreviated or unreadable.
- Method: the retailer's own catalog first. A store item number matched on the retailer's site (for Costco: costco.com and its same-day storefront) is the strongest evidence. Then the wider web.
- Output: candidates with source URLs, posted to Arya (POST /api/review/suggest) and shown in the review queue next to the item or line. Research is evidence, not a verdict: the user settles with one tap, and settling teaches the dictionary.
- A brand initial alone is not evidence. Items with no catalog match stay open.
6. The reasoning agents in the core
Each agent has a narrow job and a named limit.
- Staple judge. Input: pattern facts only (buys, cadence, quantities, all computed in code). Output: STAPLE, SEASONAL, OCCASIONAL, ONE_OFF, or NOT_CONSUMABLE per product. The verdict gates admission to the list. Explicit user intent always beats the judge. Unjudged and unresolved items stay off the list.
- Curator. See §4. Proposes identity changes; never applies them.
- Ask Arya. Chat over a household snapshot. Actions (add to list,
remove from list, ran out, mark expired, never suggest, snooze) are
validated server-side. A list is found by a near name ("Costco" finds
"Costcozy"), and only a row already on a list can be removed. The
snapshot carries receipt facts (count, last, biggest, trips per
store), spend per store per month, pack dates as words, and the
cheapest store per item. Numbers come from the data, never from the
model. At most 10 actions
run in a turn. More than 3 removes in one turn wait for a yes (Yes, do these
or Not now) on every screen. On the Ask tab the other actions apply at
once; from a list or a hunt, adds apply with Undo and the riskier actions
(out, expired, never, snooze) wait for a yes (
/api/chat/apply). Demo sessions can chat; it is a live door under the demo's per-IP and per-visit caps (§10). - Ask guard (
api/ask_guard.py). The person's latest message is checked against high-confidence phrases only (an override with a target, a prompt leak, secrets, other accounts). A hit gets a fixed refusal with no model call, and counts a strike. Three strikes in 24 hours pause Ask: a demo visit for 24 hours, a real account until the owner unlocks it in Ops (the message says to email support). The owner is never locked. A reply that quotes the system prompt, holds something shaped like a key or links off Arya is swapped for the refusal, with no strike. Stored text (item and list names, the screen, a photo) never strikes. Each Ask reply is capped at 1,500 tokens.
7. Money rules (summary; SPEC.md owns the detail)
- Returns are first-class: FIFO netting against buys, refunds reconcile into spend, patterns count kept items only, a returned item is never re-suggested.
- Every line carries an allocation: HOUSEHOLD (default) or FOR-OTHERS. For-others spend is in the ledger, never in patterns, inventory, or the list.
- The list is a derived view of the inventory model, never the source of truth.
8. Accounts, tokens, and the connector
- Per-user state is a set of JSON documents, one per former file
(receipts, dictionary, flags, lists, quest...).
ARYA_STOREpicks where they live:json(files under data/users//, the default), dual(files are the truth, every save is mirrored to Postgres), orpg(tablearya.user_docs, seeapi/pgstore.py). The demo account always stays on files. - An account name is also a folder name, so it holds only letters,
digits and
. _ % + - @(auth.valid_name). Passwords are PBKDF2 hashes; sign-in is throttled; sessions are cookies, and session tokens are stored as SHA-256 hashes. - Email code sign-in (Supabase Auth) is built behind
ARYA_SUPABASE_AUTH=1, off by default and on in the live site. Google sign-in is Arya's own (api/google_oauth.py) when its client id and secret are set, with the Supabase Google provider as the fallback. The provider only proves the email; Arya keeps its own account (named by the email, no password) and its own cookie (api/supabase_auth.py). - A connector token is
arya_plus a random secret. Only its SHA-256 hash is stored. It is shown once at mint. Minting a new one replaces the old, and it can be revoked at any time. Bearer tokens are accepted alongside session cookies, and the same token works for the Lists API (/api/v1). A token cannot reach paid bulk reads, Ops, groups, push, export or the Kroger lookup (demo_guard._TOKEN_BLOCKED). Demo accounts cannot mint. - The Muse connector (custom.arya) carries its own instruction doc: operations, when to file, the verification rules, the Resolver procedure, filing hygiene. The doc is the standing prompt. The user writes no prompt.
- Routing is contextual. A receipt, an order, or a question about the user's own list, spend, or inventory goes to Arya. A general question does not. "Arya, …" always routes. Ambiguous input: ask, or state the assumption.
9. Onboarding
- Create an account. A new account opens on Today; there is no Connect an AI agent step on first run.
- Connecting Muse is optional, from Settings > About Arya > Use Arya in Muse: make the code there (shown once).
- Tell the Muse agent to connect to Arya; it asks for the code and opens a secure credentials vault. Paste the code there, never into a chat message.
- Talk to Muse. The screen carries these steps and a copyable fallback setup
prompt for assistant apps that do not load the connector doc automatically.
The Muse Code settings-file route is the backup (
AGENT-CONNECTOR.md).
10. Cost control
- MODEL-POLICY.md is normative: deterministic work in code at $0; Spark 1.3 Standard, low effort, cached prefixes for volume reasoning (if the Contributor operator option is on, a client-side 140 requests/min gate applies; its limit is 150); Wonder is banned from volume paths.
- Every model call is written to a cost ledger. The app's daily
ceiling (
ARYA_DAILY_BUDGET_USD, $10) stops app model spend; the demo household spends from a separate bucket (ARYA_DEMO_DAILY_BUDGET_USD, $100/day in compose) and each demo visit from its own share (ARYA_DEMO_SESSION_BUDGET_USD, $2 in compose), so demo use cannot drain the app. Every live door, batch judge run and hunt creation passes the guard first. - A model answer that cannot be read is a 502 with a plain sentence,
never a 500 (
meta_client.parse_json). Each account has its own limits: a monthly heads-up, a monthly pause on paid reads, and an hourly freeze (defaults $3, $5 and $1, see the flag table; the live site uses $3, $20 and $10, and the owner account is exempt), on top of the daily ceiling for the whole app. - api/meta_client.py is the only module that calls the Model API. The personal-data rule is enforced there by construction.
11. Deployment
- One Oracle VM (Always Free; no new cloud resources). Docker Compose:
the API and Caddy with Let's Encrypt. No database container: data is
JSON files, mirrored to Supabase on the live site (
ARYA_STORE=dual); after judging Postgres can become the master copy (pg). Public name: arya.teamnightshift.org (Cloudflare A record, DNS only). The old arya-pilot.duckdns.org name is a permanent Caddy redirect to it. - Known limit: one uvicorn process on one VM. Demo sandboxes live in that
process's memory and the JSON files have one writer, so it cannot run as
several processes yet. Dual-mode saves mirror to Supabase inside the
request, so a slow Supabase can slow a save. Roughly a few dozen people at
once is the ceiling (README, Known limits). After judging: Postgres as
master copy, session state out of memory, several processes, a cloud
provider (see
docs/ROADMAP.md; the phased design isdocs/SCALABILITY-DESIGN.md, a plan, not built). - Deploy is
git pullon the VM, then a compose rebuild. Never rsync --delete: it once deleted the VM-only deploy environment. - Settings come from
deploy/.env(VM only). Compose passes only the variables it lists, so every setting the app reads is listed indocker-compose.yml;tests/test_compose_env.pyfails if one is missing..env.examplenames them. - The full test suite runs before a PR merges. A GitHub Actions workflow
(
.github/workflows/tests.yml) runs it on demand only, not on every PR (my choice). Nothing is committed tomaindirectly; a session log goes toloops/andSTATUS.mdis updated.
11.1 Logs and backups (VM runbook)
- Log caps. Compose caps each container's log at 5 files of 10 MB
(
x-loggingindocker-compose.yml), so logs cannot fill the disk. The cap applies when a container is recreated:docker compose up -d. - Nightly backup.
deploy/backup.sharchives the data volume (and thearyaPostgres schema whenARYA_BACKUP_DATABASE_URLis set indeploy/.env) into~/arya-backups, keeping the newest 7 of each. Install once on the VM withcrontab -e:17 3 * * * /path/to/arya/assistant/deploy/backup.sh >> /path/to/arya-backup.log 2>&1Check it withls -l ~/arya-backupsthe next morning. - Off-VM copy. A backup on the same disk dies with the VM. Install
rclone (
sudo apt install rclone), runrclone configonce to add a remote you own (Oracle Object Storage, Google Drive, any S3), then addARYA_BACKUP_REMOTE=<remote>:arya-backupstodeploy/.env. The script then copies each night's files there and exits non-zero (so the log shows it) if the copy fails. - Restore.
deploy/restore.sh <arya-data-*.tar.gz>stops the api, replaces the data volume and starts it again. The Postgres dump is restored by hand withpsql.
12. Quest storage
Per account, in quest.json: sessions, Shop-Offs, friends, Spotter
reports, and sightings (item, store, shelf label, time; no image).
shelf_frames.json holds one fresh shelf photo per store and item,
already scrubbed of people by SAM (shelfscan.scrub_people; no scrub,
no frame). GET /api/quest/plan counts purchases per store from the
ledger for Plan a hunt ($0). The demo sandbox overlays all of it in
memory. The store directory (stores.json) is platform-level.
13. Not built / held
- A fixed per-account spend allowance that resets by itself. Not built; per-account limits do exist (§10: defaults $3 heads-up, $5 pause, $1 hourly freeze; live $3, $20, $10).
- Public registration is closed by default (
ARYA_REGISTRATION_OPEN; the Ops flag overrides the env and is closed on the live site). The owner can open it in Settings. Every door runs on Meta models: Muse in chat, Spark on the Meta API in the app. - Forgot-password: not built. Once switched on, email code sign-in (§8) lets a person in without a password.