Overview

Deployment (first environment, 2026-10-17)

The first Cloudflare environment: Workers, D1, KV, R2, queues and secrets.

The production domain is clarisay.com (Cloudflare Registrar, zone in the same account). All three Workers serve it through Workers Custom Domains (routes with custom_domain: true in each wrangler.jsonc; wrangler deploy creates the DNS records and certificates). Their workers.dev URLs stay enabled: TestFlight builds up to 21 call the backend's workers.dev URL. Everything below was created with wrangler from the respective track directory; ids live in each wrangler.jsonc. The dashboard's mock and integration environments set routes: [] so they never claim the production hostname (wrangler environments inherit routes).

Project URL Also reachable at Worker
Backend API https://api.clarisay.com (/health, /v1/...) https://shuke-api.dou-tech.workers.dev shuke-api
Operator dashboard https://dash.clarisay.com https://shuke-dashboard.dou-tech.workers.dev shuke-dashboard
Landing https://clarisay.com (/, /zh), https://www.clarisay.com https://shuke-landing.dou-tech.workers.dev shuke-landing

ios/Config/Release.xcconfig (API_BASE_URL) points at https://api.clarisay.com/v1 for the next TestFlight build. dashboard/wrangler.jsonc BACKEND_URL names https://api.clarisay.com/v1; the request still travels over the service binding.

Resources

Resource Name / id
D1 shuke — 5208d8af-6b45-4abe-bf6d-4b40743f725a (migrations 0001–0002 applied, seeds/ loaded)
KV CONFIG aec399b3372f4f9b81fb8cd5ece9c9a5, IDEMPOTENCY 0d739a118cca450790c0b93f55d4ee8e, landing WAITLIST c4ad99d059f544ac8a410b874892bcd4
R2 shuke-audio
Queues shuke-pipeline (producer + consumer), shuke-pipeline-dlq
Vectorize shuke-content (1024 dims, cosine, metadata index language) — bound as CONTENT_INDEX
Crons 0 18 * * *, 30 18 * * *, 5 * * * *, 0 19 * * *

Secrets

Backend: JWT_SECRET, ADMIN_KEY (set with wrangler secret put). The generated values are in ~/.shuke/production-secrets.env on the machine that deployed (mode 600, not in git). Rotate with openssl rand -hex 32 | pnpm wrangler secret put JWT_SECRET (existing access tokens expire within an hour).

Dashboard: no secrets. Cloudflare Access (below) authenticates operators; the proxy forwards the Access assertion.

AI providers (production, since 2026-10-17)

ENVIRONMENT=production; secrets ANTHROPIC_API_KEY (a key created inside a named Anthropic workspace — keys from the Default workspace are rejected with "not scoped to a workspace") and OPENAI_API_KEY. Active chains: ASR Workers AI whisper-large-v3-turbo → OpenAI whisper-1; LLM/content/QA Anthropic claude-sonnet-5-5 → OpenAI gpt-4.1; TTS OpenAI gpt-4o-mini-tts → Workers AI MeloTTS; embeddings Workers AI bge-m3. The saved config lives in KV ai:providers (Dashboard → Providers); the mock providers are refused under production.

Sign in with Apple (done 2026-10-17): secrets APPLE_TEAM_ID (Z9KD6Z92BF), APPLE_KEY_ID (XZ9YBHF9AH, key "Shuke Sign in with Apple", primary App ID com.weizhi.shuke) and APPLE_PRIVATE_KEY (the one-time .p8; keep the backup outside Downloads). Verified: a client secret signed with the key gets invalid_grant (not invalid_client) from https://appleid.apple.com/auth/token, i.e. Apple accepts the key for this client id.

RevenueCat (purchases)

Set up 2026-10-17 in the RevenueCat dashboard: project Clarisay → app "Clarisay (App Store)" (com.weizhi.shuke, In-App Purchase key T5AP5D989H, App Store Connect API key D9FG72BMZ7), products clarisay.pro.annual / clarisay.pro.monthly, entitlement pro, offering default ($rc_annual, $rc_monthly). The app's public SDK key is in ios/Config/Shared.xcconfig (REVENUECAT_API_KEY). The first product records (shuke.pro.*, created before the Clarisay rename) are detached from pro and out of the offering; they never existed in App Store Connect and can be deleted. Operator steps still open:

  1. App Store Connect: create the auto-renewable subscriptions clarisay.pro.annual (7-day free trial intro offer) and clarisay.pro.monthly in one subscription group. Prices (doubled from the first plan on 2026-10-17): annual $79.99 / ¥396, monthly $13.99 / ¥60 (US base price; use the nearest available price point where a storefront has no exact match). RevenueCat shows them as "Not found" until then.
  2. RevenueCat → project Clarisay → API keys → New secret API key (v1), then cd backend && pnpm wrangler secret put REVENUECAT_API_KEY. Secret keys are scoped to one RevenueCat project: a key from another project (JuiceUp, OS, …) cannot read Clarisay's customers.
  3. Generate a webhook secret (openssl rand -hex 32), store it with pnpm wrangler secret put REVENUECAT_WEBHOOK_AUTH (the exact header value, e.g. Bearer <hex>), and in RevenueCat → Integrations → Webhooks add https://api.clarisay.com/v1/webhooks/revenuecat with that Authorization header value. Send the dashboard's test event; it should return 200.
  4. Optional: RevenueCat app settings → Apple Server Notification URL → "Apply in App Store Connect", and set the forwarding URL to https://api.clarisay.com/v1/webhooks/apple so pre-RevenueCat builds stay current.

Without steps 2–3, /v1/subscriptions/revenuecat/sync answers 503 subscription_unavailable and the webhook 401.

Cloudflare Access for the dashboard (done 2026-10-17)

The dashboard calls the backend through a service binding (services: BACKEND → shuke-api in dashboard/wrangler.jsonc): two Workers on one workers.dev subdomain cannot fetch each other (error 1042).

Zero Trust team dou-tech (dou-tech.cloudflareaccess.com), Access → Applications → shuke-dashboard (self-hosted, destination = the shuke-dashboard Worker's production and preview URLs, application id 5e9e3eca-b81b-4c52-b406-07495cbb4f51), policy Allow owner (reusable, shared with the other dashboards), session 24 h. ACCESS_TEAM_DOMAIN is set on both Workers and ACCESS_AUD on the backend (wrangler.jsonc vars).

  • Unauthenticated GET / on the dashboard → 302 to the Access login; the proxy forwards Cf-Access-Jwt-Assertion and never injects an admin key.
  • The backend verifies the assertion against the team's public keys and, under ENVIRONMENT=production, refuses X-Admin-Key (401). Scripts that used the admin key against production (e.g. smoke.sh's admin section) no longer work there; run them against a local wrangler dev or set up an Access service token and send CF-Access-Client-Id/Secret.
  • To add an operator: Zero Trust → Access controls → Policies → Allow owner → add the email (or create a second policy and attach it to the application).
  • dash.clarisay.com is a public hostname destination of the same shuke-dashboard application (one application, so the AUD tag ACCESS_AUD on the backend is unchanged). Unauthenticated requests get a 302 to the Access login.
  • Once operators use dash.clarisay.com, set "workers_dev": false in dashboard/wrangler.jsonc, deploy, and remove the Worker URL destinations from the Access application.

Verification done on 2026-10-17

clarisay.com, after deploying c8dcb19 (backend 3353c750, landing 93476b3e, dashboard ab739534):

  • https://api.clarisay.com/health and /v1/config 200; /v1/paywall without a token 401; CORS preflight from Origin: https://clarisay.com returns Access-Control-Allow-Origin: https://clarisay.com.
  • https://clarisay.com /, /zh, /terms, /privacy, /sitemap.xml, /robots.txt, /api/health 200; canonical and sitemap URLs use https://clarisay.com. https://www.clarisay.com/ 200.
  • https://dash.clarisay.com/ and /api/proxy/admin/overview → 302 to the dou-tech Access login.
  • The workers.dev URLs still answer (backend /health 200, dashboard 302 to Access).

First environment (workers.dev):

  • BASE=https://shuke-api.dou-tech.workers.dev SMOKE_ALLOW_NO_FETCH=1 pnpm smoke → smoke OK (anonymous auth, onboarding, upload, queue pipeline to assessed, settlement replay, paywall 402, brief → review → publish → public catalog). The source adaptation is skipped: CONTENT_FETCH_ENABLED=false on the Worker (ADR-002) and the fixture server is local. The smoke article was unpublished and its sources archived afterwards.
  • Dashboard /api/health 200; /api/proxy/admin/overview|review|sources|quality 200 with real data through the service binding (checked with a temporary ADMIN_KEY, since removed).
  • Landing /, /zh, /privacy, /zh/support, /sitemap.xml, /api/health 200; POST /api/waitlist 201 (the test entry was deleted from KV).

Known gaps

  • CONTENT_FETCH_ENABLED stays false until the starter sources are reviewed (ADR-002).
  • PUBLIC_BASE_URL is empty on purpose: media URLs use the request origin, so old builds on workers.dev and new builds on api.clarisay.com each get URLs on their own host.
  • www.clarisay.com serves the same pages as the apex (canonical tags point at the apex); a Cloudflare Redirect Rule www → apex would make it a 301.
  • Worker, D1, R2, KV and queue names still say shuke; renaming them is part of the brand rename, not the domain.
  • The backend's optional WAITLIST KV binding (waitlist export) is not configured.