Skip to content
NU-BSEPublic

About

Backend to creepy

Resources

Stars

4 stars

Watchers

0 watching

Forks

Repository files navigation

Creepy.IM backend

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 account
  • subscription_plans, subscriptions, subscription_invoices, subscription_events, subscription_entitlements — the gate for agent usage

Everything else stays on the device: chat history, auth providers, attestation.

Endpoints

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.

Landing download invitations (POST /marketing/download-invite)

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:

  1. POST /v3/contacts with updateEnabled: true and listIds: [BREVO_CONTACT_LIST_ID]. This creates a new contact or safely updates an existing one and guarantees list membership before automation.
  2. POST /v3/events with event name app_download_link_requested and the contact's email_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.

The agent gate (POST /chat/http)

  1. Verify Authorization: Bearer <accessToken>.
  2. 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}
  3. Count the message (Redis INCR, daily TTL).
  4. 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.

Local development

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 8000

The 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.

Granting access (manual MVP)

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}'

Checks

make check    # ruff + mypy + pytest

Tests run against sqlite + the in-memory TTL store — no services required.

Frontend integration

  • 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 calls POST {base}/chat/http via xhrHttpStream (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 via xhrHttpStream(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.

Roadmap

  • Stripe checkout + webhooks → reconcile into subscriptions, subscription_invoices, subscription_events; recompute entitlements on each lifecycle event (schema already in place).
  • auth_sessions if server-side revocation becomes necessary.
  • Observability: metrics for agent calls/rejections.

About

Backend to creepy

Resources

Stars

4 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages