FastAPI + PostgreSQL backend for Creepy.IM. Separate service from the Express attestation server; it owns accounts, subscription gating and the cloud agent proxy.
Persistence scope is deliberately minimal (see sql/migrate_001.sql, kept in
sync with the frontend's db/schema.sql):
users— the accountsubscription_plans,subscriptions,subscription_invoices,subscription_events,subscription_entitlements— the gate for agent usage
Everything else stays on the device: chat history, auth providers, attestation.
| Method | Path | Auth | Purpose |
|---|---|---|---|
| POST | /auth/email/request-code |
– | send a 6-digit code by email (Brevo API) |
| POST | /auth/email/verify-code |
– | verify code, upsert user, issue JWTs |
| POST | /auth/refresh |
refresh token | new access token |
| POST | /auth/logout |
– | client-side discard (contract only) |
| POST | /marketing/download-invite |
– | upsert a Brevo contact, add it to the invite list, emit the download automation event |
| GET | /users/me |
bearer | profile |
| PATCH | /users/me |
bearer | update name |
| GET | /subscriptions/plans |
– | public plan catalogue |
| GET | /subscriptions/me |
bearer | active subscription + entitlements |
| POST | /admin/grant |
admin bearer | manual subscription grant (MVP switch) |
| POST | /admin/grant-beta |
admin bearer | custdev/beta override: onboarding without a paywall |
| POST | /admin/revoke-beta |
admin bearer | remove a beta override |
| GET | /onboarding/me |
bearer | current onboarding v2 state |
| PATCH | /onboarding/me/intents |
bearer | save initial intents + custom intent |
| PATCH | /onboarding/me/ai-mode |
bearer | save local/cloud AI preference |
| POST | /onboarding/me/first-task/start |
bearer | mark the first real task started |
| POST | /onboarding/me/first-task/complete |
bearer | record first-task outcome and tools |
| POST | /onboarding/me/feedback |
bearer | save post-first-task feedback |
| POST | /onboarding/me/complete |
bearer | finish onboarding |
| POST | /chat/http |
bearer | gated AG-UI streaming agent proxy |
| GET | /healthz |
– | db + config checks |
Auth responses are camelCase (accessToken, refreshToken,
onboardingCompleted) to match src/auth/emailAuth.ts in the frontend.
onboardingCompleted reflects the onboarding v2 state: a brand-new account is
not complete (it goes through the flow); a pre-v2 account with a name is
migrated to completed.
The public landing site sends {email, billing, sourcePath} here instead of
trying to create contacts from browser JavaScript. The server performs two
Brevo API calls in order:
POST /v3/contactswithupdateEnabled: trueandlistIds: [BREVO_CONTACT_LIST_ID]. This creates a new contact or safely updates an existing one and guarantees list membership before automation.POST /v3/eventswith event nameapp_download_link_requestedand the contact'semail_id. Event properties include billing, source path, CTA name and trial days.
The Brevo automation should use app_download_link_requested as its Custom
event trigger and then send the Android invitation email. An additional “Add
contact to a list” action is no longer required because the endpoint does that
before it emits the event.
Quick duplicate submissions are idempotent for
MARKETING_INVITE_COOLDOWN_SECONDS; public requests are also capped per IP per
hour. Production must keep BREVO_API_KEY server-side, set
BREVO_CONTACT_LIST_ID to the intended list (currently 2), and include the
landing origin (for example https://creepy.im) in CORS_ORIGINS.
- Verify
Authorization: Bearer <accessToken>. - Resolve entitlements from an active subscription:
- no
agent_access→403 {code: "AGENT_ACCESS_REQUIRED"} - no
cloud_agent_allowed→403 {code: "CLOUD_AGENT_NOT_ALLOWED"} - daily cap exceeded →
402 {code: "USAGE_LIMIT_REACHED", retryAfterSeconds}
- no
- Count the message (Redis
INCR, daily TTL). - Proxy to the upstream LLM and stream AG-UI events back as NDJSON —
one JSON event per line, the exact framing
xhrHttpStream(@tanstack/ai-client) parses:
{"type":"RUN_STARTED","threadId":...,"runId":...}
{"type":"TEXT_MESSAGE_START","messageId":...,"role":"assistant"}
{"type":"TEXT_MESSAGE_CONTENT","messageId":...,"delta":"..."} (xN)
{"type":"TEXT_MESSAGE_END","messageId":...}
{"type":"RUN_FINISHED","threadId":... ,"runId":...}
Failed runs terminate with RUN_ERROR (never a silent close).
Email codes, rate limits and usage counters live in Redis (or an in-process
TTL store when REDIS_URL is empty — dev only). Tokens are stateless JWTs:
~15 min access, ~30 day refresh; no session table.
cp .env.example .env # set JWT_SECRET (openssl rand -hex 32)
python -m venv .venv
.venv/bin/pip install -e ".[dev]"
docker compose up -d postgres redis
.venv/bin/uvicorn app.main:app --reload --port 8000The schema (sql/migrate_001.sql) is applied idempotently on startup and the
free/pro plans are seeded. Swagger UI: http://localhost:8000/docs.
With LLM_MOCK=true (default in .env.example) the agent streams a canned
reply, so the whole gate can be exercised without any LLM keys. With no
BREVO_API_KEY, verification codes and marketing calls are suppressed outside
production. Production also requires a Brevo-verified sender in EMAIL_FROM.
Register through the app (or the two auth endpoints), then as a user listed in
ADMIN_EMAILS:
curl -X POST http://localhost:8000/admin/grant \
-H "Authorization: Bearer <adminAccessToken>" \
-H "Content-Type: application/json" \
-d '{"userId":"usr_...","planCode":"pro","days":30}'make check # ruff + mypy + pytestTests run against sqlite + the in-memory TTL store — no services required.
EXPO_PUBLIC_API_URL→ this service's base URL (auth + users + subscriptions).EXPO_PUBLIC_TANSTACK_AI_BASE_URL→ this service's base URL; the client then callsPOST {base}/chat/httpviaxhrHttpStream(src/ai/index.ts).- The landing site uses the same service base URL for
POST /marketing/download-invite. - The remote connection must send
Authorization: Bearer <accessToken>— pass it viaxhrHttpStream(url, { headers: { Authorization: ... } }). Without the header the gate cannot work. - Optional: surface the 402/403 rejection
codes (CLOUD_AGENT_NOT_ALLOWED,USAGE_LIMIT_REACHED) in the chat UI as an upgrade prompt.
- Stripe checkout + webhooks → reconcile into
subscriptions,subscription_invoices,subscription_events; recompute entitlements on each lifecycle event (schema already in place). auth_sessionsif server-side revocation becomes necessary.- Observability: metrics for agent calls/rejections.