From 446b7a1a633011f183781bc7dfc4358e96469e1d Mon Sep 17 00:00:00 2001 From: Muawiya Amir Date: Sun, 25 Oct 2026 13:06:29 +0500 Subject: [PATCH 01/17] fix(edge): restore public auth and privacy landing pages --- supabase/functions/api/handler.test.mjs | 26 ++++ supabase/functions/api/handler.ts | 3 + supabase/functions/api/public_pages.ts | 156 ++++++++++++++++++++++++ 3 files changed, 185 insertions(+) create mode 100644 supabase/functions/api/public_pages.ts diff --git a/supabase/functions/api/handler.test.mjs b/supabase/functions/api/handler.test.mjs index 6b08476c..394b2e5f 100644 --- a/supabase/functions/api/handler.test.mjs +++ b/supabase/functions/api/handler.test.mjs @@ -14,6 +14,32 @@ const config = { const request = (path, init = {}) => new Request(`https://one-concept.supabase.co/functions/v1/api${path}`, init); const editorialToken = (claims) => `header.${Buffer.from(JSON.stringify(claims)).toString('base64url')}.signature`; +test('public cutover pages serve browser HTML without leaking service credentials', async () => { + const calls = []; + const handler = createHandler(config, async (...args) => { + calls.push(args); + throw new Error('Public pages must not use a service request'); + }); + for (const path of ['/confirmed', '/reset-password', '/privacy']) { + const result = await handler(request(path)); + const body = await result.text(); + assert.equal(result.status, 200); + assert.match(result.headers.get('content-type'), /^text\/html/); + assert.equal(result.headers.get('cache-control'), 'no-store'); + assert.equal(result.headers.get('referrer-policy'), 'no-referrer'); + assert.match(result.headers.get('content-security-policy'), /frame-ancestors 'none'/); + assert.doesNotMatch(body, /private-service-key|__NONCE__|__CONFIG__/); + } + assert.equal(calls.length, 0); + const reset = await (await handler(request('/reset-password'))).text(); + assert.match(reset, /\/auth\/v1\/user/); + assert.match(reset, /public-anon-key/); + assert.match(reset, /history\.replaceState\(null, '', location\.pathname\)/); + const rejected = await handler(request('/reset-password', { method: 'POST' })); + assert.equal(rejected.status, 405); + assert.equal(rejected.headers.get('allow'), 'GET'); +}); + test('editorial correction request reaches the atomic job RPC with the receipt fingerprint', async () => { const sessionId = 'a5a94932-62e0-4780-91c7-af48b93e27ab'; const revisionId = 'f285ee8a-0bdd-4a94-ac16-ce96c9c3a272'; diff --git a/supabase/functions/api/handler.ts b/supabase/functions/api/handler.ts index f4d480c1..ecd32373 100644 --- a/supabase/functions/api/handler.ts +++ b/supabase/functions/api/handler.ts @@ -2,6 +2,7 @@ import { conceptPreview, editorialDigest, revisionPreview, stageLessonBody } from './editorial_preview.ts'; import { AvatarInputError, normalizeAvatar } from './avatar_image.ts'; +import { publicPage } from './public_pages.ts'; export type ApiConfig = { supabaseUrl: string; @@ -1058,6 +1059,8 @@ export function createHandler(config: ApiConfig, fetcher: Fetcher = fetch, const requestUrl = new URL(request.url); const path = routePath(requestUrl.pathname); if (path === null) return httpError(404, 'Not Found', cors); + const page = publicPage(path, request.method, config, cors); + if (page) return page; const concept = /^\/v1\/concepts\/([^/]+?)(?:\/(like|save))?$/.exec(path); const publicProfile = /^\/v1\/public-profiles\/([^/]+)$/.exec(path); const connectionStatus = /^\/v1\/me\/connections\/with\/([^/]+)$/.exec(path); diff --git a/supabase/functions/api/public_pages.ts b/supabase/functions/api/public_pages.ts new file mode 100644 index 00000000..0edaf90c --- /dev/null +++ b/supabase/functions/api/public_pages.ts @@ -0,0 +1,156 @@ +/** Browser landing pages retained when the learner API moved off Railway. */ + +type PageConfig = { supabaseUrl: string; anonKey: string }; + +const STYLE = ` + body { margin: 0; min-height: 100vh; display: grid; place-items: center; + background: #0f1115; color: #e8eaf0; font: 16px/1.6 system-ui, sans-serif; } + main { width: 100%; max-width: 420px; padding: 2rem; box-sizing: border-box; } + h1 { font-size: 1.6rem; margin: 0 0 .5rem; } + p, label { color: #aeb5c3; } + form { display: grid; gap: .75rem; } + label { display: block; font-size: .9rem; } + input { width: 100%; box-sizing: border-box; padding: .75rem; border-radius: 10px; + border: 1px solid #3d4658; background: #171a21; color: #e8eaf0; font: inherit; } + input:focus { outline: 2px solid #8da5ff; } + button { padding: .8rem; border: 0; border-radius: 10px; background: #8da5ff; + color: #0f1115; font: inherit; font-weight: 700; cursor: pointer; } + button:disabled { opacity: .6; cursor: default; } + .error { color: #ffaaaa; } .success { color: #9cebb5; } + .hidden { display: none; } +`; + +const CONFIRMED = ` + +One Concept — email confirmed +

Email confirmed ✓

+

You're all set. Open the One Concept app and sign in to get today's concept.

+
`; + +const RESET = ` + +One Concept — reset password +

Reset your password

+

Choose a new password for your One Concept account.

+ +
`; + +// Keep this learner-facing disclosure aligned with backend/app/privacy.py. +const PRIVACY = ` + +One Concept — Privacy Policy

Privacy Policy

Effective: October 1, 2026

+

One Concept is operated by Coding Moves. This policy explains the data used to provide the app and how to contact us about it.

+

Information used to provide the app

+

How we use information

We use this information to sign you in, select and remember learning activities, calculate streaks, keep saved and liked content, and send enabled reminders. We do not use the app for advertising or include behavioural analytics or crash-reporting SDKs.

+

Optional public profile

Profile sharing is off by default. If enabled, anyone with your link or QR code sees only the display name, streaks, learned-concept count and achievements you select. Your email, saved concepts, account identifiers and private activity are excluded. Turning sharing off revokes the link, but cannot erase screenshots already made.

+

Optional Connections

Connections stores requests, accepted relationships and blocks for your private lists. Receiving requests is off by default. Other learners see only the profile information you currently share. You can decline, cancel, remove or block a connection. Turn off profile sharing to revoke a public link. Limited request-event timestamps enforce invitation limits.

+

Service providers

Supabase provides authentication, database and API infrastructure. Expo delivers enabled push notifications. These providers process the information needed to run the app.

+

Choices and retention

You can turn reminders off in the app. Signing out removes account-scoped app caches and attempts to deregister the current device while online. Account and learning data remain associated with your account until you request deletion or we no longer need them to run the service.

+

Security

Private requests use a verified account token, and access controls are designed to limit data to the signed-in account. No internet transmission or storage method is completely secure.

+

Your requests and contact

Request access to, correction of, or deletion of your account data by emailing contactmuawia@gmail.com from your account address. We may need to verify the request.

+

Changes

We may update this policy as data practices change. The current version is published at this address with its effective date.

+
`; + +function configLiteral(config: PageConfig): string { + return JSON.stringify({ url: config.supabaseUrl.replace(/\/+$/, ''), anonKey: config.anonKey }) + .replace(/[<>&\u2028\u2029]/g, character => + `\\u${character.charCodeAt(0).toString(16).padStart(4, '0')}`); +} + +export function publicPage(path: string, method: string, config: PageConfig, cors: Headers): Response | null { + if (!['/confirmed', '/reset-password', '/privacy'].includes(path)) return null; + if (method !== 'GET') { + const headers = new Headers(cors); + headers.set('content-type', 'application/json; charset=utf-8'); + headers.set('allow', 'GET'); + return new Response(JSON.stringify({ detail: 'Method Not Allowed' }), { status: 405, headers }); + } + const nonce = crypto.randomUUID().replaceAll('-', ''); + const template = path === '/confirmed' ? CONFIRMED : path === '/privacy' ? PRIVACY : RESET; + const body = template.replaceAll('__NONCE__', nonce) + .replace('__CONFIG__', () => configLiteral(config)); + const headers = new Headers(cors); + headers.set('content-type', 'text/html; charset=utf-8'); + headers.set('cache-control', 'no-store'); + headers.set('x-content-type-options', 'nosniff'); + headers.set('referrer-policy', 'no-referrer'); + headers.set('x-frame-options', 'DENY'); + headers.set('content-security-policy', `default-src 'none'; base-uri 'none'; frame-ancestors 'none'; form-action 'self'; style-src 'nonce-${nonce}'; script-src 'nonce-${nonce}'; connect-src ${config.supabaseUrl}`); + return new Response(body, { status: 200, headers }); +} From 024472cf3c5ffe741110e1090c28ef582e64fe56 Mon Sep 17 00:00:00 2001 From: Muawiya Amir Date: Sun, 25 Oct 2026 13:06:50 +0500 Subject: [PATCH 02/17] fix(clients): point privacy links at Supabase API --- admin/public/privacy.html | 2 +- mobile/src/screens/AboutScreen.tsx | 6 +++--- mobile/src/services/authRedirect.ts | 2 +- mobile/tests/authRedirect.test.mjs | 1 + 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/admin/public/privacy.html b/admin/public/privacy.html index 6f102a68..6e76c080 100644 --- a/admin/public/privacy.html +++ b/admin/public/privacy.html @@ -13,7 +13,7 @@

Privacy policy

How we handle review workspace data

Effective October 4, 2026

-

One Concept Review is a private editorial workspace operated by Coding Moves. This policy describes its accounts, review records, and optional email notifications. The One Concept learning app privacy policy covers learner accounts and app data.

+

One Concept Review is a private editorial workspace operated by Coding Moves. This policy describes its accounts, review records, and optional email notifications. The One Concept learning app privacy policy covers learner accounts and app data.

Editorial accounts and work

Supabase Auth handles sign-in, password recovery, and authenticator enrollment. We use an invited reviewer's email address, approved name, permissions, and timezone to manage access and assignments. Our backend records assigned lessons, review comments, requested corrections, decisions, publication history, and related timestamps. Approved reviewer names may appear in the details of lessons they verify.

diff --git a/mobile/src/screens/AboutScreen.tsx b/mobile/src/screens/AboutScreen.tsx index b8f0b9a0..3bad3cdb 100644 --- a/mobile/src/screens/AboutScreen.tsx +++ b/mobile/src/screens/AboutScreen.tsx @@ -7,6 +7,7 @@ import { Alert, Linking, Pressable, ScrollView, StyleSheet, Text, View } from 'r import { useTheme } from '../context/ThemeContext'; import { ScreenHeader } from '../components/ScreenHeader'; import { API_BASE_URL } from '../api/client'; +import { authLandingUrl } from '../services/authRedirect'; import { scaleIcon, scaleFont, radius, spacing, ThemeColors, typography } from '../theme'; import { ProfileStackParamList } from './ProfileScreen'; @@ -23,9 +24,8 @@ const ISSUES_URL = 'https://github.com/Coding-Moves/one-concept/issues/new'; const FEEDBACK_EMAIL = 'contactmuawia@gmail.com'; function privacyUrl(): string { - return API_BASE_URL - ? new URL('/privacy', API_BASE_URL).toString() - : 'https://github.com/Coding-Moves/one-concept/blob/develop/backend/app/privacy.py'; + return authLandingUrl('privacy', API_BASE_URL) + ?? 'https://github.com/Coding-Moves/one-concept/blob/develop/backend/app/privacy.py'; } /** diff --git a/mobile/src/services/authRedirect.ts b/mobile/src/services/authRedirect.ts index 0b2e6d06..5912751a 100644 --- a/mobile/src/services/authRedirect.ts +++ b/mobile/src/services/authRedirect.ts @@ -1,4 +1,4 @@ -export type AuthLandingPath = 'confirmed' | 'reset-password'; +export type AuthLandingPath = 'confirmed' | 'reset-password' | 'privacy'; /** * Builds a browser landing URL on the configured API origin. diff --git a/mobile/tests/authRedirect.test.mjs b/mobile/tests/authRedirect.test.mjs index 844d79d9..a9ec046b 100644 --- a/mobile/tests/authRedirect.test.mjs +++ b/mobile/tests/authRedirect.test.mjs @@ -6,6 +6,7 @@ test('builds confirmation and recovery landings on the configured API origin', ( assert.equal(authLandingUrl('confirmed', 'https://api.example.org'), 'https://api.example.org/confirmed'); assert.equal(authLandingUrl('reset-password', 'https://api.example.org/'), 'https://api.example.org/reset-password'); assert.equal(authLandingUrl('confirmed', 'https://api.example.org/api'), 'https://api.example.org/api/confirmed'); + assert.equal(authLandingUrl('privacy', 'https://example.supabase.co/functions/v1/api'), 'https://example.supabase.co/functions/v1/api/privacy'); }); test('never passes a malformed or credential-bearing redirect to Supabase', () => { From dbc9748e20934871950d26ad743b7b2a391bde74 Mon Sep 17 00:00:00 2001 From: Muawiya Amir Date: Sun, 25 Oct 2026 13:07:23 +0500 Subject: [PATCH 03/17] docs: replace Railway auth redirect runbook for Supabase --- docs/AUTH_REDIRECTS.md | 130 ++++++++++++++--------------------------- docs/CODEBASE_MAP.md | 11 ++-- docs/WORK_LOG.md | 6 ++ 3 files changed, 57 insertions(+), 90 deletions(-) diff --git a/docs/AUTH_REDIRECTS.md b/docs/AUTH_REDIRECTS.md index cb39b996..7bae1d34 100644 --- a/docs/AUTH_REDIRECTS.md +++ b/docs/AUTH_REDIRECTS.md @@ -1,94 +1,54 @@ -# Authentication email redirects +# Authentication email redirects after the Supabase cutover -Supabase verifies email addresses itself. After verification, it redirects the -browser to a permitted HTTPS URL. One Concept uses its FastAPI public pages for -that final browser step: +Supabase Auth verifies email links, then sends the browser to a permitted HTTPS +landing URL. The public Supabase `api` Edge function now serves three pages: -- `GET /confirmed` displays the One Concept confirmation page and asks the - person to return to the app. -- `GET /reset-password` displays the One Concept password-reset form. +- `/functions/v1/api/confirmed` explains successful email confirmation. If an + older client sends a recovery session to the Site URL, this page forwards the + URL fragment to the reset page without sending it to the server. +- `/functions/v1/api/reset-password` accepts a recovery session in the URL + fragment and updates the password directly through Supabase Auth. It removes + the token from the address bar as soon as the page loads. +- `/functions/v1/api/privacy` is the learner privacy policy. -These pages are product pages served by the API. Railway is only the current -hosting provider; its name, error page, or dashboard must never be part of the -learner experience. +The mobile app derives its explicit signup and recovery redirect URLs from +`EXPO_PUBLIC_API_BASE_URL`. Its production value must be the matching +`https://PROJECT.supabase.co/functions/v1/api` address. The Edge routes are +public browser pages; private application routes still authenticate the user. -The mobile app supplies `emailRedirectTo` for new sign-ups and `redirectTo` for -password recovery from `EXPO_PUBLIC_API_BASE_URL`. This keeps both flows on the -same deployed API origin instead of relying only on a stale Supabase Site URL. -Supabase must allow those destinations before it will honor them. +## Production configuration -## Owner checklist - -Do these **before merging the PR**. They are safe while the PR is open and do -not require a mobile build, database migration, secret, or payment. - -1. In Railway, open the production **api** service, then **Settings → - Networking**. Copy the active public HTTPS domain exactly. Do not copy a - deployment ID, internal service URL, or an old removed domain. -2. In a private browser window, open `https://YOUR-API-DOMAIN/confirmed`. - Continue only when it displays the One Concept **“Email confirmed”** page. - If Railway shows “The train has not arrived at the station”, wait for the - deployment/domain to become active or create and attach a new Railway domain - to the current api service. Do not place that broken address in Supabase. -3. Open Supabase **Authentication → URL Configuration** for the production - project. Set **Site URL** to `https://YOUR-API-DOMAIN`. This remains the - fallback for old app versions and any request that lacks a redirect value. -4. Under **Redirect URLs**, add these exact entries: +1. Deploy the tested Edge `api` function revision containing these pages. + Check `/health`, `/confirmed`, `/reset-password` and `/privacy` over HTTPS. + A direct visit to the reset page without a recovery session should show an + invalid-link message, not a blank page or JSON 404. +2. In the matching Supabase project, open **Authentication → URL Configuration**. + Allow these exact redirect URLs, with no wildcard or trailing slash: ```text - https://YOUR-API-DOMAIN/confirmed - https://YOUR-API-DOMAIN/reset-password + https://PROJECT.supabase.co/functions/v1/api/confirmed + https://PROJECT.supabase.co/functions/v1/api/reset-password ``` - Keep any still-working previous recovery URL until links already sent to - users have expired. Do not add wildcards, credentials, a Railway dashboard - URL, `localhost`, or a URL with a query string. -5. In Expo/EAS, inspect the **production** value of - `EXPO_PUBLIC_API_BASE_URL`. It must equal `https://YOUR-API-DOMAIN` with no - trailing path, query, fragment, username, or password. The value is public - configuration; never put Supabase service keys or database passwords there. -6. Save the dashboard settings and capture the exact domain in the release - record. No source-code configuration file should contain the live domain if - EAS already owns that public setting. - -## After this PR merges to `develop` - -1. Wait for the normal preview EAS update workflow to complete, then install or - open the staging/preview app. A merge to `develop` does **not** update the - production app. -2. Create a fresh test account with an inbox you control. Open the confirmation - email and check this sequence: - - ```text - Supabase verifies the address → One Concept confirmation page → sign in succeeds - ``` - - The browser must not show a Railway 404 page. Test password recovery too; - it must open the One Concept reset-password page and permit sign-in with the - new password. -3. If the test fails, keep the PR merged but stop the production release. Check - the exact generated email URL, the Railway domain and the Supabase allowlist - rather than changing templates or exposing a credential. - -## Before production users receive the fix - -This is a JavaScript mobile change. After the preview test passes, include it in -a normal `develop` → `main` release PR. After that release PR is merged, verify -the deployed API health and manually run the repository’s **Release** workflow -on the verified `main` commit. That publishes the production EAS OTA update to -compatible installed builds. A production user receives it on a later app -launch; an update is not instantaneous. - -Changing the Supabase Site URL in the checklist is still worthwhile before the -OTA: it repairs the fallback redirect used by older app versions. Do not claim -production confirmation is fixed until a new controlled account has completed -the real email flow. - -## Future own-domain upgrade - -When One Concept has a domain, point a subdomain such as -`app.oneconcept.example` at the API service, wait for HTTPS provisioning, then -repeat the checklist using that domain. A true browser-to-app open button later -requires Android App Links (and iOS Universal Links) hosted on that domain. It -is separate from email verification and should be introduced with its own -native release, association-file validation, and device testing. +3. After the pages are live, set the **Site URL** fallback to the exact + `/functions/v1/api/confirmed` page. Its recovery forwarding handles older + clients that omitted an explicit reset redirect. Keep previous working + redirect URLs until outstanding email links expire; a removed Railway host + cannot be repaired by an allowlist entry. +4. Check Expo's **production** `EXPO_PUBLIC_API_BASE_URL` points to the same + Supabase `api` function, and keep the Supabase Auth URL and publishable key + matched to that project. Never put a service-role key in mobile or a page. + +## Real-device verification + +With an inbox and account you control, request a fresh signup confirmation and +a fresh password recovery email. Confirm that the first link opens the One +Concept confirmation page and allows sign-in. Confirm that the second opens +the One Concept reset form, accepts a new password, and allows sign-in with it. +Do not share email links, URL fragments, tokens, or passwords in logs, screenshots +or chat. A 200 response from the page alone does not prove the full Auth flow. + +The production app must receive the compatible JavaScript update for its About +screen to link to the Edge privacy page under the API path. Older builds can +still contain removed Railway URLs; ask those users to update the app rather +than re-enabling Railway. diff --git a/docs/CODEBASE_MAP.md b/docs/CODEBASE_MAP.md index a0348419..1d83b0ac 100644 --- a/docs/CODEBASE_MAP.md +++ b/docs/CODEBASE_MAP.md @@ -12,7 +12,7 @@ learned history, streaks, likes, saved concepts, and push reminders. | Area | Entry points and purpose | | --- | --- | | Mobile | `mobile/README.md` is the local developer on-ramp; `mobile/index.ts` registers `mobile/App.tsx`; Expo SDK 57, React Native 0.86, React 19, TypeScript. `mobile/src/screens/AboutScreen.tsx` links learners to the public review explanation. | -| Review website | `admin/README.md`, `admin/src/main.tsx`, `App.tsx`; independent React/TypeScript/Vite static editorial workspace with Supabase Auth and private FastAPI calls. `admin/public/about.html` and `privacy.html` are public OAuth information pages. `admin/src/hosting.ts` generates exact-origin static response headers during build; `admin/public/_redirects` handles Netlify SPA deep links. Direct deployment steps are in `docs/EDITORIAL_ROLLOUT.md`. | +| Review website | `admin/README.md`, `admin/src/main.tsx`, `App.tsx`; independent React/TypeScript/Vite static editorial workspace with Supabase Auth and private Supabase Edge API calls. `admin/public/about.html` and `privacy.html` are public OAuth information pages. `admin/src/hosting.ts` generates exact-origin static response headers during build; `admin/public/_redirects` handles Netlify SPA deep links. Direct deployment steps are in `docs/EDITORIAL_ROLLOUT.md`. | | Backend | `backend/app/main.py`; FastAPI, async SQLAlchemy/asyncpg, Pydantic settings, ES256 JWT verification. Docker uses Python 3.12. | | Database | `backend/migrations/`; Supabase PostgreSQL schema, RLS, seeds, and incremental migrations. | | Content lifecycle | `docs/CONTENT_ARCHITECTURE.md`, `docs/CONTENT_OPERATIONS.md`; portable subject/curriculum imports, durable refill, reviewed publication, daily review, protected health report. | @@ -364,13 +364,14 @@ private diagnostics and restore procedures. ## Backend request and service flow -The Supabase API candidate for #416 lives in `supabase/functions/api/` and +The production Supabase API lives in `supabase/functions/api/` and `backend/migrations/0046_supabase_api_basics.sql`–`0116_supabase_profile_avatar_api.sql`. All 73 JSON paths have candidate handlers, including private avatar upload, short-lived Storage signing, and editorial read/command routes. The avatar image module bundles a pinned ImageMagick WASM file for 512px JPEG conversion. -These candidate routes remain isolated from production clients. The complete route checklist and -remaining parity gates are in [SUPABASE_API_ROUTE_PARITY.md](SUPABASE_API_ROUTE_PARITY.md). +Its `public_pages.ts` serves the browser confirmation, recovery and learner +privacy pages at the same API path. The historical route checklist and parity +gates are in [SUPABASE_API_ROUTE_PARITY.md](SUPABASE_API_ROUTE_PARITY.md). `backend/tools/apply_staging_migrations.py` applies ordered migrations only to a fresh staging project after checking its project reference against the connection. Its `--inspect` mode reports staging schema metadata and profile row presence @@ -408,7 +409,7 @@ Authenticated routes pass through `core/rate_limit.py` after JWT verification. I | `me.py`: `PUT /v1/me/topics`, `PATCH /v1/me` | Whole-set follows, profile name, PostgreSQL-validated timezone. | | `me.py`: `GET/PUT /v1/me/notifications`, `POST/DELETE /v1/me/push-token` | Reminder preferences and scoped device registration/removal. | | `concepts.py`: `GET /v1/concepts/{slug}`, `PUT/DELETE .../like`, `.../save` | Published lesson detail and independent interaction writes. | -| `pages.py`: `GET /privacy`, `/confirmed`, `/reset-password` | Public privacy and branded Auth landing pages; email redirects use the mobile API origin and reset uses Supabase Auth in the browser. See [auth redirects](AUTH_REDIRECTS.md). | +| `pages.py`: `GET /privacy`, `/confirmed`, `/reset-password` | Historical FastAPI equivalents of the production Edge `public_pages.ts` routes. Email redirects use the mobile API origin and recovery calls Supabase Auth in the browser. See [auth redirects](AUTH_REDIRECTS.md). | `router.py` mounts authenticated feature routers under `/v1`. Response/input models live in `schemas/daily.py`, `me.py`, `notifications.py`, and `topics.py`. diff --git a/docs/WORK_LOG.md b/docs/WORK_LOG.md index 1e44bc64..23f0126a 100644 --- a/docs/WORK_LOG.md +++ b/docs/WORK_LOG.md @@ -7,6 +7,12 @@ claims as completed work. ## Current status +### Production Supabase cutover landing pages and content supply — 2026-10-25 + +- The owner deleted the Railway services after confirming the production app, review site and real 08:00 reminder worked on Supabase. Production SQL showed `daily_reminders`, `editorial_generation`, `legacy_enrichment`, and `http-reconcile` active; `content_pool` and `prefetch` remain off. For Software Engineering, the owner's aggregate query showed 26 published lessons, 2 draft concepts, 20 pending-review revisions, zero pending curated titles and 6 failed titles. Some pending revisions may correct published lessons, so they are not 20 new lessons. Keep refill paused while the owner reviews exact content and failed-title causes. +- Live production `GET /functions/v1/api/confirmed`, `/reset-password`, and `/privacy` each returned 404 after Railway deletion. Commit `446b7a1` adds public Edge browser pages while preserving the existing app's explicit API-based Auth redirects. Commit `024472c` fixes the mobile About privacy URL to retain the `/functions/v1/api` path and updates the reviewer policy link. The owner is adding the two exact Edge redirect allowlist entries. Do not claim the live pages work until the tested API revision is deployed and real signup/recovery flows pass. +- Validation: Edge TypeScript and handler tests passed; mobile typecheck and all 103 Node tests passed (the in-sandbox child-process check needed an unsandboxed run); a local headless browser accepted a mocked recovery token, submitted a password to a mocked Supabase Auth endpoint, removed the token fragment, rejected a bare reset URL and forwarded a fallback recovery link from `/confirmed`. No real password or account was used. The branch still needs PR review, merge authorization, production release/deploy, and real email/phone verification. + ### Combined learner fixes #454–#457 — 2026-10-24 - Branch `codex/fix-454-457-product-polish` starts from current `develop` at `dae3d95`. The owner requested one PR covering Markdown readability and code samples (#454), opt-in followed topics (#455), keyboard-safe profile editing (#456), and gallery/camera avatar uploads (#457). Target is `develop`; this task does not authorize a release or PR merge. From baf6e4dbd73cd5e27f98d6c2663b52bd28c91976 Mon Sep 17 00:00:00 2001 From: Muawiya Amir Date: Sun, 25 Oct 2026 13:16:43 +0500 Subject: [PATCH 04/17] docs(admin): describe Supabase hosting in review privacy policy --- admin/public/privacy.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/admin/public/privacy.html b/admin/public/privacy.html index 6e76c080..dd555088 100644 --- a/admin/public/privacy.html +++ b/admin/public/privacy.html @@ -23,7 +23,7 @@

Reviewer notification email

We keep bounded delivery records such as message batch, attempt time, provider acceptance or failure, and sanitized error status to troubleshoot notifications. Gmail acceptance does not prove inbox delivery. The owner can turn reviewer email sending off, and the Gmail account holder can revoke the app's access in their Google Account.

Hosting, use, and retention

-

Netlify serves the website; Railway hosts the API and workers; Supabase provides authentication and database infrastructure; Google delivers optional reviewer emails. These providers process information needed to operate their respective services. We do not use review data or Google account access for advertising. Hosting providers may process technical request information to deliver and secure the service.

+

Netlify serves the website; Supabase provides authentication, database, API and scheduled worker infrastructure; Google delivers optional reviewer emails. These providers process information needed to operate their respective services. We do not use review data or Google account access for advertising. Hosting providers may process technical request information to deliver and secure the service.

We retain account and editorial records while needed to run the review workflow and preserve the integrity of published decisions. Delivery attempts are retained for operational diagnosis. If you want to access, correct, or request deletion of your personal information, contact us below; we may need to verify your identity and retain records that are necessary for audit or service integrity.

Security and contact

From 95280c0faae35f9fef2108730433ae68a25cc3dc Mon Sep 17 00:00:00 2001 From: Muawiya Amir Date: Wed, 28 Oct 2026 17:10:17 +0500 Subject: [PATCH 05/17] chore: remove retired hosting deployment configuration --- .github/scripts/check-release-revision.sh | 2 +- .github/workflows/release.yml | 4 ++-- backend/.env.example | 2 +- backend/Dockerfile | 6 +++--- backend/app/services/editorial_mail.py | 2 +- backend/app/workers/pool_topup.py | 5 +++-- backend/app/workers/reminders.py | 5 +++-- backend/railway.json | 11 ----------- backend/tests/test_owner_reporting.py | 2 +- mobile/src/context/AuthContext.tsx | 4 ++-- supabase/functions/api/handler.ts | 2 +- 11 files changed, 18 insertions(+), 27 deletions(-) delete mode 100644 backend/railway.json diff --git a/.github/scripts/check-release-revision.sh b/.github/scripts/check-release-revision.sh index 570bf2eb..327b45d4 100644 --- a/.github/scripts/check-release-revision.sh +++ b/.github/scripts/check-release-revision.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash set -euo pipefail -# This validates an operator's deployment attestation, not Railway health itself. +# This validates an operator's deployment attestation, not live API health itself. if [[ "${GITHUB_REF:-}" != refs/heads/main ]]; then echo '::error::Publish releases from main only.' exit 1 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 931e2621..1f87a0b3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,5 +1,5 @@ -# Railway deploys main independently. Publish only after the operator verifies -# the same commit on the API and workers and checks production health. +# Deploy the Supabase API and worker through the protected production workflow +# first. Publish only after the operator verifies that exact main commit and health. name: Release on: diff --git a/backend/.env.example b/backend/.env.example index 44c67d46..6919bc4b 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -74,5 +74,5 @@ GENERATION_MAX_CONCURRENT=3 WEEKLY_QUIZ_NOTIFICATIONS_ENABLED=false # Optional bounded owner dashboard observations. Enable only after migration 0039. -# Same setting on API/reminders/pool-topup; rollout belongs to #281. +# Keep this value consistent across local API and worker processes. OWNER_TELEMETRY_ENABLED=false diff --git a/backend/Dockerfile b/backend/Dockerfile index b1666cdb..97c71511 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,4 +1,4 @@ -# Railway builds and runs this image. +# Optional local container for the Python API and maintenance tools. FROM python:3.12-slim ENV PYTHONUNBUFFERED=1 \ @@ -15,9 +15,9 @@ COPY content ./content COPY migrations ./migrations COPY schema ./schema -# Railway supplies $PORT; default keeps `podman run` and local use working. +# Keep the port overridable for local container use. ENV PORT=8000 EXPOSE 8000 -# Never bake secrets into the image — they come from Railway's variable store. +# Never bake secrets into the image; supply them at runtime. CMD ["sh", "-c", "uvicorn app.main:app --host 0.0.0.0 --port ${PORT}"] diff --git a/backend/app/services/editorial_mail.py b/backend/app/services/editorial_mail.py index cfa5ed13..9bcc4b63 100644 --- a/backend/app/services/editorial_mail.py +++ b/backend/app/services/editorial_mail.py @@ -72,7 +72,7 @@ def message(settings, batch): async def send(settings, batch): - """Gmail's HTTPS API works without Railway's paid SMTP networking.""" + """Send through Gmail's HTTPS API without requiring an SMTP connection.""" raw = base64.urlsafe_b64encode(message(settings, batch).as_bytes()).decode("ascii") try: async with ( diff --git a/backend/app/workers/pool_topup.py b/backend/app/workers/pool_topup.py index 3ef03bba..8a465fd0 100644 --- a/backend/app/workers/pool_topup.py +++ b/backend/app/workers/pool_topup.py @@ -1,7 +1,8 @@ """Run with: python -m app.workers.pool_topup -Scheduled on Railway as a cron job. Keeps every topic stocked so the request -path never waits on a model. +Legacy Python entry point for controlled maintenance. Production content jobs run +through the Supabase Edge worker. This command prepares private content without +blocking the learner request path. """ import asyncio diff --git a/backend/app/workers/reminders.py b/backend/app/workers/reminders.py index f2662053..d9f7ac46 100644 --- a/backend/app/workers/reminders.py +++ b/backend/app/workers/reminders.py @@ -1,7 +1,8 @@ """Run with: python -m app.workers.reminders -Scheduled on Railway every 15 minutes. Nudges users whose reminder time just -passed in their own timezone and who have not finished today's concept. +Legacy Python entry point for controlled maintenance. Production reminders run +through the Supabase Edge worker. This command nudges users whose reminder time +just passed in their own timezone and who have not finished today's concept. """ import asyncio diff --git a/backend/railway.json b/backend/railway.json deleted file mode 100644 index 77ac1d16..00000000 --- a/backend/railway.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "$schema": "https://railway.app/railway.schema.json", - "build": { "builder": "DOCKERFILE", "dockerfilePath": "Dockerfile" }, - "deploy": { - "preDeployCommand": ["python -m app.workers.schema_check"], - "healthcheckPath": "/health", - "healthcheckTimeout": 30, - "restartPolicyType": "ON_FAILURE", - "restartPolicyMaxRetries": 3 - } -} diff --git a/backend/tests/test_owner_reporting.py b/backend/tests/test_owner_reporting.py index 60142211..d75f678d 100644 --- a/backend/tests/test_owner_reporting.py +++ b/backend/tests/test_owner_reporting.py @@ -315,7 +315,7 @@ async def test_worker_observations_do_not_depend_on_api_telemetry_switch(api, se {"id": uuid4()}, ) await session.commit() - # Workers and the API have independent environment variables on Railway. + # Worker and API processes can have independent environment variables. api.settings.owner_telemetry_enabled = False response = await api.client.get("/v1/editorial/owner/operations", headers=api.headers()) assert response.status_code == 200, response.text diff --git a/mobile/src/context/AuthContext.tsx b/mobile/src/context/AuthContext.tsx index 8013b877..ffd2d5f9 100644 --- a/mobile/src/context/AuthContext.tsx +++ b/mobile/src/context/AuthContext.tsx @@ -79,8 +79,8 @@ export function AuthProvider({ children }: { children: ReactNode }) { }, []); const signUp = useCallback(async (email: string, password: string) => { - // Do not rely on Supabase's global Site URL: it can retain an old - // Railway hostname. The verified email link returns to the same API origin + // Do not rely on Supabase's global Site URL: it can retain a retired + // hostname. The verified email link returns to the same API origin // that this build uses, provided that URL is allowed in Supabase. const emailRedirectTo = authLandingUrl('confirmed', API_BASE_URL); const { data, error } = await supabase.auth.signUp({ diff --git a/supabase/functions/api/handler.ts b/supabase/functions/api/handler.ts index ecd32373..747a65ab 100644 --- a/supabase/functions/api/handler.ts +++ b/supabase/functions/api/handler.ts @@ -1,4 +1,4 @@ -/** Candidate Railway-compatible JSON routes. No production client uses these yet. */ +/** Supabase Edge API routes for learner and editorial clients. */ import { conceptPreview, editorialDigest, revisionPreview, stageLessonBody } from './editorial_preview.ts'; import { AvatarInputError, normalizeAvatar } from './avatar_image.ts'; From b863b34e4f0315f45080901d8ba57217fcbff6af Mon Sep 17 00:00:00 2001 From: Muawiya Amir Date: Wed, 28 Oct 2026 17:12:39 +0500 Subject: [PATCH 06/17] docs: make protected Supabase deploy the release path --- RELEASING.md | 69 ++++++++++++++++++++----------------- docs/SCHEMA_VERIFICATION.md | 48 +++++++++----------------- 2 files changed, 54 insertions(+), 63 deletions(-) diff --git a/RELEASING.md b/RELEASING.md index 8014cc80..49344eee 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -7,8 +7,10 @@ Production release flow. Keep it boring and repeatable. (This is our internal flow — the project doesn't take outside PRs; see [CONTRIBUTING.md](CONTRIBUTING.md).) - `main` is production. A release is a single PR **develop → main** (no `release/*` branch). -- Merging to `main` triggers Railway backend deployment. Mobile publication is - a separate manual run of `.github/workflows/release.yml` after verification. +- After merging to `main`, dispatch the protected + `.github/workflows/supabase-production-deploy.yml` workflow for that exact + commit. Mobile publication is a separate manual run of + `.github/workflows/release.yml` after verification. ## Cutting a release 1. **Bump the version on `develop` first.** Edit `mobile/app.config.js` → `expo.version` @@ -32,14 +34,17 @@ Production release flow. Keep it boring and repeatable. release PR.** 4. Open the release PR **develop → main**. It must pass the required **"Migrations applied check"** and get its approval, then merge. -5. On merge, Railway auto-deploys the API from `main`. Keep generation paused - during backend/worker transitions. Verify the new main commit is deployed to - the API and workers, `/health` succeeds, and the release-specific smoke checks - pass. Do not resume old generators against the new editorial schema. +5. After the merge, dispatch **Deploy Supabase API and worker to production** on + the exact current `main` commit and approve its `production-schema` gate. + It verifies the actual schema, deploys both Edge Functions, checks that the + worker rejects unsigned calls, and checks API `/health`. Keep generation + gates unchanged during deployment, then verify the release-specific API, + scheduled-job, and client smoke checks. A merge alone does not deploy them. 6. Open **GitHub Actions → Release → Run workflow**, select **main**, and enter - the full 40-character main commit SHA you verified on production API/workers - in `backend_revision`. This is an operator attestation; the workflow does not - inspect Railway deployments itself. Do not submit it until checks are complete. + the full 40-character main commit SHA you verified in the protected Edge + deployment and production checks in `backend_revision`. This is an operator + attestation; Release does not independently inspect the deployed function + revision. Do not submit it until checks are complete. 7. The workflow first requires protected actual-schema verification (see [setup and checks](docs/SCHEMA_VERIFICATION.md)). It rejects another branch, an older deployment or a main revision that advanced before validation. @@ -50,8 +55,7 @@ Production release flow. Keep it boring and repeatable. ## Database migrations — MANUAL, every release The deploy does **not** auto-migrate. Files in `backend/migrations/*.sql` must be run -by hand against the production Supabase DB, or the app queries columns/tables that -don't exist (this caused the `pool-topup` "column claimed_at does not exist" crash). +by hand against the production Supabase DB before deploying code that uses them. For each migration not yet applied to prod: 1. Run it — Supabase → **SQL Editor** (paste the file), or @@ -60,9 +64,9 @@ For each migration not yet applied to prod: `.github/workflows/migrations.yml` fails on `main` and on release PRs if any migration isn't listed in `applied.txt`. That required filename check proves only that the -operator recorded it. The main-only protected schema workflow and Railway -pre-deploy check inspect actual database objects; follow -[SCHEMA_VERIFICATION.md](docs/SCHEMA_VERIFICATION.md) to configure both gates. +operator recorded it. The protected production Edge deployment and Release +schema jobs inspect actual database objects; follow +[SCHEMA_VERIFICATION.md](docs/SCHEMA_VERIFICATION.md) to configure those gates. The Release workflow requires the protected check before publishing. Migrations are immutable once applied — never edit an applied file; add a new one. @@ -70,16 +74,19 @@ are immutable once applied — never edit an applied file; add a new one. Installed apps update over the air on next launch — no reinstall. The stable APK download link in the README changes only on a native (runtimeVersion) release. -## One-time backend config (already set in production) -- `SUPABASE_ANON_KEY` on the Railway `api` service — the `/reset-password` page needs it. -- Supabase → Auth → Redirect URLs must include `/reset-password`. +## Auth redirect configuration -## Backend endpoint migration without a new app version +The Edge API needs its public Supabase publishable/anon key to render the +password-reset page. In production Supabase Auth, allow the exact +`/functions/v1/api/confirmed` and `/functions/v1/api/reset-password` URLs on +the production project origin. Verify both pages are deployed before using +them as email redirect or Site URLs. See [AUTH_REDIRECTS.md](docs/AUTH_REDIRECTS.md). -Follow [the Railway migration runbook](docs/RAILWAY_MIGRATION.md) for service -handover, recovery redirects, device checks and old-endpoint retirement. The -mobile API address comes from EAS `EXPO_PUBLIC_API_BASE_URL`; editing Railway -or merging the runbook alone does not update installed applications. +## API-only operational updates + +The mobile API address comes from EAS `EXPO_PUBLIC_API_BASE_URL`; changing an +Edge Function or repository file alone does not update already-published mobile +bundles. Confirm the target URL and redirect allowlist in each environment. An endpoint-only operational update reuses the existing release's source and native runtime. It does not create another version tag or a new release PR. @@ -88,26 +95,26 @@ apply. Do not rerun `release.yml` for an already-published version to repoint it API: OTA publication runs before that workflow rejects a duplicate version tag, so the run can partially publish and then fail. -After backend/worker revision verification, recovery configuration and candidate -testing, obtain the owner's approval of the specific production endpoint switch. +After protected API/worker deployment, recovery configuration and candidate +testing, obtain the owner's approval of a specific production endpoint change. Use a clean checkout of the verified current `main` revision, install its locked dependencies, and verify `EXPO_PUBLIC_API_BASE_URL` in each EAS environment. Check the effective runtime and app version before export; do not use a feature branch or bump native runtime merely to move the API. Recheck remote `main` immediately -before publication and stop if it differs from the verified checkout/deployments. +before publication and stop if it differs from the verified checkout/deployment. Publish the candidate with the preview environment first, using a compatible preview installation for validation. Once approved, the explicit EAS commands from that checkout's `mobile/` directory are: ```sh -eas update --channel preview --environment preview --message "Verify Railway API migration" --non-interactive +eas update --channel preview --environment preview --message "Verify API endpoint" --non-interactive # Only after candidate validation and approval of the production switch: -eas update --channel production --environment production --message "Migrate Railway API endpoint" --non-interactive +eas update --channel production --environment production --message "Update API endpoint" --non-interactive ``` Record the source SHA, environment URL, runtime, update group, channel and device -results in the migration PR. Do not imply all users have updated when an upload -succeeds. Retain the old API until older runtimes, offline clients, fresh installs, -recovery links and rollback are accounted for. Environment-variable rollback -alone does not change already-published bundles. +results in the release record. Do not imply all users have updated when an upload +succeeds. Account for older runtimes, offline clients, fresh installs, recovery +links and rollback. Environment-variable rollback alone does not change +already-published bundles. diff --git a/docs/SCHEMA_VERIFICATION.md b/docs/SCHEMA_VERIFICATION.md index f2f0b188..a855acf6 100644 --- a/docs/SCHEMA_VERIFICATION.md +++ b/docs/SCHEMA_VERIFICATION.md @@ -42,28 +42,14 @@ invalid/missing connection configuration, permission errors or connection failur exit nonzero. A failure must be investigated, not bypassed by editing the ledger or copying the failing target's schema into the contract. -## One-time Railway setup +## Production Edge deployment gate -After an image containing this verifier is available, configure **each** of the -API, reminders and pool-topup services: - -1. Preserve the existing real start command and cron schedule. -2. Verify its private `DIRECT_URL` targets the intended application database. -3. Set **Settings → Deploy → Pre-deploy command** to - `python -m app.workers.schema_check` (optional platform timeout: 60 seconds). -4. Confirm effective deployment settings and a successful pre-deploy log before - completing the rollout. A failed check must stop the new deployment. - -The Docker image now includes both the contract and immutable migration files. -`backend/railway.json` declares the command for services already consuming that -file. The migrated services may use dashboard configuration; merging this PR -alone cannot enable their dashboard gate. Do not opt new services into legacy -Config as Code or assume the API's settings configure the workers. - -Railway's pre-deploy command runs in a separate container and a nonzero result -prevents deployment. This check does not run generation, send reminders, or -change records. Do not remove a working deployment to test failure on production; -use the disposable test database described below. +The protected `supabase-production-deploy.yml` workflow verifies the current +`main` commit and the actual target schema before publishing the API and worker +Edge Functions. Its `production-schema` environment holds the private direct +database URL and project-scoped access token. A failed schema check stops the +deployment. This read-only check does not run generation, send reminders, or +change records. Use a disposable test database to test failure cases. ## Protected GitHub checks and mobile releases @@ -83,17 +69,16 @@ secret the schema check fails and Release cannot publish OTA or a GitHub release checks out the current commit, rejects a stale main revision, and reads the environment secret only in the verification step. No checkout-ref input or `pull_request_target` workflow permits PR code to run with production access. -- `release.yml` uses its own direct protected job before OTA publication. A - reusable workflow call did not receive the environment secret during the - v1.10.3 main push, although direct dispatch succeeded. The existing - backend-revision attestation still verifies operator intent; the schema gate - does not independently identify the Git SHA deployed on Railway. +- `supabase-production-deploy.yml` verifies schema and current `main` before + publishing both Edge Functions, then checks worker boot and API health. +- `release.yml` uses its own direct protected job before OTA publication. Its + backend-revision attestation verifies operator intent; the schema gate does + not independently identify the deployed Edge Function revision. A feature-branch manual dispatch skips the protected job; it is not production -verification. GitHub environment/branch protections and Railway settings require -owner setup; workflow files alone cannot establish those dashboard protections. -The post-merge check does not itself block Railway's independent auto-deploy; -the Railway pre-deploy setting provides that protection. +verification. GitHub environment/branch protections require owner setup; +workflow files alone cannot establish those dashboard protections. A merge to +`main` does not deploy Edge Functions until the protected workflow succeeds. ## Adding a migration and maintaining the contract @@ -124,5 +109,4 @@ The portable verifier is extracted from draft PR #231; its VM deployment, SSH, billing, and unrelated app changes remain deferred. When that draft resumes, reconcile this implementation rather than adding a second verifier. -References: [Railway pre-deploy commands](https://docs.railway.com/deployments/pre-deploy-command), -[GitHub reusable workflows and environment secrets](https://docs.github.com/en/actions/how-tos/reuse-automations/reuse-workflows). +Reference: [GitHub reusable workflows and environment secrets](https://docs.github.com/en/actions/how-tos/reuse-automations/reuse-workflows). From 5a5dda6da061488e6a50be6bcce40dc125c2ede0 Mon Sep 17 00:00:00 2001 From: Muawiya Amir Date: Wed, 28 Oct 2026 17:13:35 +0500 Subject: [PATCH 07/17] docs: describe Supabase API and worker operations --- backend/README.md | 34 +++++++++++++++++----------------- docs/ARCHITECTURE.md | 34 +++++++++++++++++++--------------- docs/INCIDENT_RESPONSE.md | 25 +++++++++++++------------ 3 files changed, 49 insertions(+), 44 deletions(-) diff --git a/backend/README.md b/backend/README.md index 4da242cf..1ba593dc 100644 --- a/backend/README.md +++ b/backend/README.md @@ -1,17 +1,17 @@ # One Concept — backend -FastAPI service that owns the Gemini API key, the daily concept selection, and -every write to the database. The mobile app never talks to Gemini or Postgres -directly. +Python API and maintenance implementation of the learning rules. The mobile app +never talks to Gemini or Postgres directly. Production serves those rules through +the Supabase Edge API and scheduled worker. ``` -Mobile app ──► FastAPI ──┬──► Supabase (Postgres + Auth) - └──► Gemini API +Mobile app ──► Supabase Edge API ──► Supabase (Postgres + Auth) +Supabase Cron ──► Edge worker ─────► Expo Push / Gmail / Gemini ``` -Status: **Phase 7 — reminders.** Reads, writes, Gemini generation from a -curated backlog, and timezone-aware push reminders that stop once the day is -learned. +Production serves the learner and reviewer API through Supabase Edge Functions; +the Python service remains a local and maintenance implementation. Scheduled +production reminders and generation run through the Supabase Edge worker. ## Sustainable content lifecycle @@ -42,7 +42,7 @@ backend/ ├── migrations/ # plain SQL, applied in filename order ├── email-templates/ # account email HTML installed manually in Supabase Auth ├── tests/ # regression suite for API, security, persistence, workers and content lifecycle -├── Dockerfile # what Railway builds +├── Dockerfile # optional local container └── .env.example # copy to .env — never commit the filled copy ``` @@ -167,8 +167,8 @@ Run the worker: python -m app.workers.pool_topup ``` -On Railway, add this as a **cron job** on the same service (daily is plenty). -It is safe to run concurrently: backlog items are claimed with +This is a controlled legacy maintenance command; production scheduling uses +the Supabase Edge worker. Backlog items are claimed with `FOR UPDATE SKIP LOCKED`, so two workers never write the same title. ### Safety rails @@ -354,11 +354,11 @@ today pending, day missed), and RLS isolation between two users. ## Deployment -**Railway**, from `backend/Dockerfile` (see `railway.json`). Set every variable -from `.env.example` in Railway's variable store — never in the image, never in -git. Point the health check at `/health`, and set `ENVIRONMENT=production` to -disable both `/docs` and `/openapi.json` (the raw schema). A cron worker for -pool top-up and reminders joins later, in Phases 6 and 7. +Production API and worker code lives in `supabase/functions/api/` and +`supabase/functions/worker/`. Deploy the exact current `main` revision through +the protected GitHub workflow in [RELEASING.md](../RELEASING.md). The Python +service in this directory can be run locally with private configuration; never +commit a filled `.env` or bake credentials into the optional Docker image. ## Deployment schema verification @@ -366,4 +366,4 @@ The migration ledger is an operator record. Before deployment, run `python -m app.workers.schema_check` with `DIRECT_URL` supplied privately. This read-only check fails on missing or changed required schema even if every SQL filename is listed. See [setup and contract maintenance](../docs/SCHEMA_VERIFICATION.md) -for the Railway pre-deploy command and protected GitHub release check. +for the protected production Edge deployment and release checks. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index f9a74ff5..83a94b21 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -4,13 +4,16 @@ Mobile app (Expo / React Native) | v -FastAPI backend (Railway) ──┬──► Supabase (Postgres + Auth) - └──► Gemini API +Supabase Edge API ───────────┬──► Supabase (Postgres + Auth) + └──► private editorial operations +Supabase Cron → Edge worker ─────► Expo Push / Gmail HTTPS / Gemini ``` -The mobile app never talks to Gemini or Postgres directly. The backend owns -the API key, every write, and the daily selection. See -[backend/README.md](../backend/README.md) for endpoints and auth details. +The mobile app uses Supabase Auth for sessions and calls the Edge API for app +data. It never talks to Gemini or Postgres directly. The API owns data writes +and daily selection; scheduled generation and notifications run through the +Edge worker. See [backend/README.md](../backend/README.md) for the shared API +contract and local Python maintenance implementation. ## Concept selection — no repeats @@ -24,21 +27,22 @@ Published concepts in followed topics → assign, save to history ``` -When a user has read everything in their topics, the backend generates one -new lesson on demand; a nightly worker keeps every topic stocked to 25 ahead -of time so nobody normally waits on a model. +When a user has read all published lessons in their followed topics, the app +offers scheduled reviews. New lesson generation is a bounded background process +from a human-planned backlog. It can stop for an empty plan, a full review queue, +provider limits, or a disabled gate; learner requests do not wait for Gemini. ## Lessons -Gemini writes lessons; it never chooses subjects — titles come from a curated -backlog. Every lesson is 2–3 plain sentences plus one concrete example, and -output is validated (length bounds, banned filler words) before publishing. -Rejected output is retried later, never shown to a user. +Gemini drafts lessons from curated titles; it does not choose subjects. Output +is validated and remains private until a human reviews and publishes the full +lesson package. Failed plans have bounded retries and require diagnosis before +an operator grants another attempt. ## Reminders Up to three reminder times per user, interpreted in the user's own timezone. -A worker runs every 15 minutes on Railway, claims each due (user, day, slot) +A Supabase scheduled worker checks due slots, claims each (user, day, slot) occurrence in a log table before pushing — so a crash or overlapping run can miss a nudge but never send a duplicate — and stops the moment the user marks the concept learned. If the learner follows no active topic, the worker preserves @@ -69,9 +73,9 @@ the completion history — a gaps-and-islands query, no counters to drift. | Layer | Technology | |---|---| | Mobile | Expo SDK 57, React Native, TypeScript | -| Backend | Python, FastAPI, SQLAlchemy (async) | +| API | Supabase Edge Function with PostgreSQL RPCs | | Database & auth | Supabase (Postgres) | | AI | Gemini (flash-lite) | | Push | Expo Notifications + Firebase Cloud Messaging | -| Hosting | Railway (API + cron workers) | +| Scheduled work | Supabase Cron + Edge worker | | CI/CD | GitHub Actions, EAS Build + Update | diff --git a/docs/INCIDENT_RESPONSE.md b/docs/INCIDENT_RESPONSE.md index 320f21a3..1bc79e76 100644 --- a/docs/INCIDENT_RESPONSE.md +++ b/docs/INCIDENT_RESPONSE.md @@ -15,18 +15,19 @@ issue #161 owns production telemetry. ## Triage -1. Record the time, affected surface (API, sign-in, reminders, pool refill, or - mobile delivery), and the deployed commit shown by Railway. Do not copy - secrets or raw request bodies. -2. Check `GET /health` on the production API. A failing check means Railway's - `backend/railway.json` health check and restart policy may already be - recovering the API; inspect the deployment state and logs for the incident - window. +1. Record the time, affected surface (API, sign-in, reminders, content refill, or + mobile delivery), and the exact `main` commit used by the protected Supabase + deployment. Do not copy secrets or raw request bodies. +2. Check `GET /functions/v1/api/health` on the production Supabase project. + Inspect Edge Function logs and the protected deploy run for the incident + window; a healthy response does not prove authenticated routes work. 3. In Supabase, check project health and authentication status. Do not alter data, RLS policies, or migrations during diagnosis unless the incident plan specifically requires it. -4. Check the scheduled Railway services separately. The API becoming healthy - does not prove reminders or pool top-up have run successfully. +4. Check `cron.job`, `edge_worker_jobs`, and recent `edge_worker_runs` for the + affected scheduled job. A healthy API does not prove reminders or content + generation ran successfully. Check the corresponding application queue and + delivery result before retrying work. 5. Check GitHub Actions only for the relevant commit: migrations, release validation, and EAS publication are release controls, not proof that a live dependency is healthy. @@ -36,9 +37,9 @@ issue #161 owns production telemetry. - Tell learners what is affected, what they can safely keep doing offline, and when the next update will be posted. Do not expose incident IDs, stack traces, database/provider names, or security details publicly. -- Prefer a rollback to the last known healthy deployment when a newly deployed - API revision is the confirmed cause. Verify `/health`, a signed-in read, and - the scheduled services afterward. +- Prefer a rollback to the last known healthy Edge Function revision when a + newly deployed API or worker revision is the confirmed cause. Verify API + `/health`, a signed-in read, and the relevant scheduled jobs afterward. - If Supabase is unavailable, do not attempt ad-hoc restores. Pause destructive operator actions, use the documented provider recovery process, and validate identities, data access, and RLS before reopening normal traffic. From b0f738f7b68311b2f3230826f0c28d5949b82794 Mon Sep 17 00:00:00 2001 From: Muawiya Amir Date: Wed, 28 Oct 2026 17:19:49 +0500 Subject: [PATCH 08/17] fix: point review site privacy and API config to Supabase --- admin/src/api.test.ts | 9 +++++++++ admin/src/config.ts | 20 +++++++++++++++----- admin/src/hosting.test.ts | 3 +++ admin/src/hosting.ts | 4 ++-- 4 files changed, 29 insertions(+), 7 deletions(-) diff --git a/admin/src/api.test.ts b/admin/src/api.test.ts index b24d1be7..91cb0421 100644 --- a/admin/src/api.test.ts +++ b/admin/src/api.test.ts @@ -156,6 +156,15 @@ describe("private request boundaries", () => { VITE_SUPABASE_PUBLISHABLE_KEY: "sb_publishable_test", }; expect(readConfig(base).apiUrl).toBe("https://api.test"); + expect(readConfig({ ...base, VITE_API_URL: "https://api.test/functions/v1/api" }).apiUrl) + .toBe("https://api.test/functions/v1/api"); + expect(() => readConfig({ ...base, VITE_API_URL: "https://api.test/other" })).toThrow(); + expect(readConfig({ ...base, VITE_API_URL: "https://project.supabase.co/functions/v1/api", + VITE_SUPABASE_URL: "https://project.supabase.co" }).apiUrl) + .toBe("https://project.supabase.co/functions/v1/api"); + expect(() => readConfig({ ...base, + VITE_API_URL: "https://staging.supabase.co/functions/v1/api", + VITE_SUPABASE_URL: "https://production.supabase.co" })).toThrow(); expect(() => readConfig({ ...base, diff --git a/admin/src/config.ts b/admin/src/config.ts index b7d2f44f..73244e1e 100644 --- a/admin/src/config.ts +++ b/admin/src/config.ts @@ -12,10 +12,13 @@ export function readConfig(env: Record) { parsed.password || parsed.search || parsed.hash || - parsed.pathname !== "/" + (name === "VITE_API_URL" + ? !["/", "/functions/v1/api"].includes(parsed.pathname) + : parsed.pathname !== "/") ) - throw new Error("Use an HTTPS origin for " + name); - return parsed.origin; + throw new Error("Use an HTTPS origin or the exact Supabase API function URL for " + name); + return name === "VITE_API_URL" && parsed.pathname !== "/" + ? parsed.origin + parsed.pathname : parsed.origin; } const key = env.VITE_SUPABASE_PUBLISHABLE_KEY || ""; if (!key.startsWith("sb_publishable_")) { @@ -26,9 +29,16 @@ export function readConfig(env: Record) { throw new Error("Use a Supabase public publishable or anon key."); } } + const apiUrl = url("VITE_API_URL"); + const supabaseUrl = url("VITE_SUPABASE_URL"); + if (new URL(apiUrl).hostname.endsWith(".supabase.co") && + (new URL(apiUrl).pathname !== "/functions/v1/api" || + new URL(apiUrl).origin !== supabaseUrl)) { + throw new Error("Supabase API and Auth must use the same project"); + } return { - apiUrl: url("VITE_API_URL"), - supabaseUrl: url("VITE_SUPABASE_URL"), + apiUrl, + supabaseUrl, key, environment: env.VITE_ENVIRONMENT || "Unspecified environment", }; diff --git a/admin/src/hosting.test.ts b/admin/src/hosting.test.ts index b72f3126..ec82c3f0 100644 --- a/admin/src/hosting.test.ts +++ b/admin/src/hosting.test.ts @@ -13,5 +13,8 @@ describe("static-host response headers", () => { expect(headers).toContain("Cache-Control: no-store"); expect(headers).toContain("X-Robots-Tag: noindex, nofollow, noarchive"); expect(headers).toContain("style-src-attr 'unsafe-inline'"); + expect(headers).toContain("img-src 'self' data: https://auth.example.test;"); + expect(pagesHeaders({ apiUrl: "https://api.example.test/functions/v1/api", supabaseUrl: "https://auth.example.test" })) + .toContain("connect-src 'self' https://api.example.test https://auth.example.test;"); }); }); diff --git a/admin/src/hosting.ts b/admin/src/hosting.ts index f37ac4d8..24925e93 100644 --- a/admin/src/hosting.ts +++ b/admin/src/hosting.ts @@ -1,7 +1,7 @@ import type { readConfig } from "./config.ts"; export function pagesHeaders(config: Pick, "apiUrl" | "supabaseUrl">): string { - const connectSrc = ["'self'", config.apiUrl, config.supabaseUrl].join(" "); + const connectSrc = ["'self'", new URL(config.apiUrl).origin, config.supabaseUrl].join(" "); return `/* Cache-Control: no-store X-Content-Type-Options: nosniff @@ -9,6 +9,6 @@ export function pagesHeaders(config: Pick, "apiUrl X-Frame-Options: DENY X-Robots-Tag: noindex, nofollow, noarchive Permissions-Policy: camera=(), microphone=(), geolocation=() - Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self'; style-src-attr 'unsafe-inline'; img-src 'self' data:; connect-src ${connectSrc}; object-src 'none'; base-uri 'none'; frame-ancestors 'none'; form-action 'self' + Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self'; style-src-attr 'unsafe-inline'; img-src 'self' data: ${config.supabaseUrl}; connect-src ${connectSrc}; object-src 'none'; base-uri 'none'; frame-ancestors 'none'; form-action 'self' `; } From 17651c1b6d9c40d62dbfcfb5e2db8f989a41ecf6 Mon Sep 17 00:00:00 2001 From: Muawiya Amir Date: Wed, 28 Oct 2026 17:22:07 +0500 Subject: [PATCH 09/17] docs: update active setup and content guides for Supabase Edge --- admin/README.md | 38 ++++---- docs/AUTH_REDIRECTS.md | 9 +- docs/CODEBASE_MAP.md | 45 +++++---- docs/FUTURE_REFILL_ROLLOUT.md | 171 +++++++++++++--------------------- docs/PROFILE_AVATARS.md | 2 +- docs/ROADMAP.md | 2 +- docs/owner-dashboard.md | 41 ++++---- 7 files changed, 135 insertions(+), 173 deletions(-) diff --git a/admin/README.md b/admin/README.md index c07d60f4..12c9c96c 100644 --- a/admin/README.md +++ b/admin/README.md @@ -2,7 +2,7 @@ A private, branded editorial website, built independently from the Expo app. React + TypeScript + Vite produce static files; Supabase Auth signs reviewers in, -and the existing FastAPI backend authorizes every editorial read and write. +and the Supabase Edge API authorizes every editorial read and write. The browser has no database, Gemini or service-role credentials. Public source code and private workspace access are separate concerns. The @@ -25,13 +25,14 @@ npm run dev | Public build variable | Meaning | | --- | --- | -| `VITE_API_URL` | Exact HTTPS origin of the compatible FastAPI API; local development accepts `http://localhost:8000` or `http://127.0.0.1:8000`. | +| `VITE_API_URL` | Exact URL of the matching Supabase Edge API, ending in `/functions/v1/api`; local Python development accepts `http://localhost:8000`. | | `VITE_SUPABASE_URL` | Matching Supabase Auth HTTPS origin. | | `VITE_SUPABASE_PUBLISHABLE_KEY` | Public `sb_publishable_…` key, or legacy JWT with role `anon`. Never a service-role/secret key. | | `VITE_ENVIRONMENT` | Visible label, such as `Staging` or `Production`. | -The build rejects missing/invalid origins and privileged keys in the public key -field. All `VITE_` values are public: never add private values under that prefix. +The build rejects missing/invalid URLs, mismatched Supabase projects, and +privileged keys in the public key field. All `VITE_` values are public: never +add private values under that prefix. No real credentials are needed for the automated tests. ```sh @@ -107,10 +108,15 @@ only HTTP(S), open separately with no opener, and lesson text is rendered as tex ## Hosting and manual activation -The reviewer workspace and Owner dashboard deploy as one static site. The -[direct production rollout guide](../docs/EDITORIAL_ROLLOUT.md) gives the free -Netlify build settings, exact API/Auth origins, account and email setup, -verification and rollback. `npm run build` writes `dist/_headers` with CSP +The reviewer workspace and Owner dashboard deploy as one Netlify static site. +Set the production branch to `main`, base directory to `admin`, build command +to `npm run build`, and publish directory to `dist`. Set only the four public +build variables above, matching the production Supabase project; do not place +service-role, Gmail or Gemini credentials in the browser build. The production +Edge API must allow the exact Netlify origin in `ALLOWED_ORIGINS`, use that +origin plus `/auth/callback` for `EDITORIAL_INVITE_REDIRECT_URL`, and use the +origin for `EDITORIAL_EMAIL_DASHBOARD_URL`. Supabase Auth must allow the exact +callback URL. `npm run build` writes `dist/_headers` with CSP `connect-src` restricted to the two configured origins; the old broad static header file is no longer used. Netlify uses `public/_redirects` for the `/auth/callback` SPA fallback. Preview deployments remain off until a separate @@ -124,20 +130,20 @@ deploy before entering them in Google Cloud. The root remains the invited-user sign-in page; it is not the public OAuth homepage. Do not substitute the mobile app's separate API privacy page for this site's privacy URL. -Merging a code PR into `develop` does not publish the website or change Railway -flags. After the compatible backend and website are live, an approved and -published card enters normal server selection automatically; each content -approval needs no app release. Software/schema changes still use the normal +Merging a code PR into `develop` does not publish the website or change +production Edge Function flags. After the compatible API and website are live, +an approved and published card enters normal server selection automatically; +each content approval needs no app release. Software/schema changes still use the normal release procedure. ## Reviewer notifications Owners can open **Notifications** to see overdue assignments, delivery failures, review policy and bounded retries. Reviewers choose their email deadline timezone -in **Settings**. Delivery uses the existing backend worker and the sender's Gmail -HTTPS API; no SMTP upgrade or new paid mail service is required. Follow the -[notification setup and direct production rollout](../docs/editorial-notifications.md) -before enabling email. Supabase Auth email templates remain separate. +in **Settings**. Delivery uses the scheduled Supabase Edge worker and the +sender's Gmail HTTPS API. Keep the production editorial email gate and daily +cap at their intended values; check `edge_worker_runs` and the owner dashboard +before enabling sends. Supabase Auth email templates remain separate. ## Owner dashboard and safe demo diff --git a/docs/AUTH_REDIRECTS.md b/docs/AUTH_REDIRECTS.md index 7bae1d34..8109a9e6 100644 --- a/docs/AUTH_REDIRECTS.md +++ b/docs/AUTH_REDIRECTS.md @@ -1,7 +1,8 @@ # Authentication email redirects after the Supabase cutover Supabase Auth verifies email links, then sends the browser to a permitted HTTPS -landing URL. The public Supabase `api` Edge function now serves three pages: +landing URL. Once the pending public-page release is deployed, the Supabase +`api` Edge function serves three pages: - `/functions/v1/api/confirmed` explains successful email confirmation. If an older client sends a recovery session to the Site URL, this page forwards the @@ -33,7 +34,7 @@ public browser pages; private application routes still authenticate the user. 3. After the pages are live, set the **Site URL** fallback to the exact `/functions/v1/api/confirmed` page. Its recovery forwarding handles older clients that omitted an explicit reset redirect. Keep previous working - redirect URLs until outstanding email links expire; a removed Railway host + redirect URLs until outstanding email links expire; a retired host cannot be repaired by an allowlist entry. 4. Check Expo's **production** `EXPO_PUBLIC_API_BASE_URL` points to the same Supabase `api` function, and keep the Supabase Auth URL and publishable key @@ -50,5 +51,5 @@ or chat. A 200 response from the page alone does not prove the full Auth flow. The production app must receive the compatible JavaScript update for its About screen to link to the Edge privacy page under the API path. Older builds can -still contain removed Railway URLs; ask those users to update the app rather -than re-enabling Railway. +still contain retired host URLs; ask those users to update the app rather +than restoring the retired service. diff --git a/docs/CODEBASE_MAP.md b/docs/CODEBASE_MAP.md index 1d83b0ac..d7867436 100644 --- a/docs/CODEBASE_MAP.md +++ b/docs/CODEBASE_MAP.md @@ -1,6 +1,6 @@ # Codebase map -Source inspection: 2026-09-13. This is a navigation guide to the implementation, +Source inspection: 2026-10-28. This is a navigation guide to the implementation, not a claim that deployed services or every runtime behavior have been verified. Refresh the relevant sections when the code changes. @@ -12,14 +12,14 @@ learned history, streaks, likes, saved concepts, and push reminders. | Area | Entry points and purpose | | --- | --- | | Mobile | `mobile/README.md` is the local developer on-ramp; `mobile/index.ts` registers `mobile/App.tsx`; Expo SDK 57, React Native 0.86, React 19, TypeScript. `mobile/src/screens/AboutScreen.tsx` links learners to the public review explanation. | -| Review website | `admin/README.md`, `admin/src/main.tsx`, `App.tsx`; independent React/TypeScript/Vite static editorial workspace with Supabase Auth and private Supabase Edge API calls. `admin/public/about.html` and `privacy.html` are public OAuth information pages. `admin/src/hosting.ts` generates exact-origin static response headers during build; `admin/public/_redirects` handles Netlify SPA deep links. Direct deployment steps are in `docs/EDITORIAL_ROLLOUT.md`. | -| Backend | `backend/app/main.py`; FastAPI, async SQLAlchemy/asyncpg, Pydantic settings, ES256 JWT verification. Docker uses Python 3.12. | +| Review website | `admin/README.md`, `admin/src/main.tsx`, `App.tsx`; independent React/TypeScript/Vite static editorial workspace with Supabase Auth and private Supabase Edge API calls. `admin/public/about.html` and `privacy.html` are public OAuth information pages. `admin/src/hosting.ts` generates exact-origin static response headers during build; `admin/public/_redirects` handles Netlify SPA deep links. Current hosting instructions are in `admin/README.md`. | +| Production API | `supabase/functions/api/handler.ts` routes mobile and review-site requests through the protected Supabase Edge API and SQL RPCs. `backend/app/main.py` remains the Python reference and maintenance implementation. | | Database | `backend/migrations/`; Supabase PostgreSQL schema, RLS, seeds, and incremental migrations. | -| Content lifecycle | `docs/CONTENT_ARCHITECTURE.md`, `docs/CONTENT_OPERATIONS.md`; portable subject/curriculum imports, durable refill, reviewed publication, daily review, protected health report. | -| Content engine | `backend/app/services/generation.py`, `pool.py`, `prefetch.py`; Gemini lessons from a curated backlog. | -| Operations | `.github/workflows/`, `backend/railway.json`, `backend/Dockerfile`, `mobile/eas.json`, `mobile/app.config.js`. | -| Hosting migration | `docs/RAILWAY_MIGRATION.md`; source/destination evidence, worker handover, EAS endpoint publication, recovery redirects and retirement. New Railway services use dashboard settings because legacy Config as Code is deprecated. `.github/workflows/supabase-production-deploy.yml` and `supabase-production-cron-install.yml` are registered here on the default branch for manual, protected runs against current `main`; they are not triggered by merging `develop`. | -| Supabase scheduled workers | `supabase/functions/worker/` validates signed Cron calls; migrations 0119–0120 own the disabled run ledger and HTTP reconciliation; migrations 0128–0129 feed owner reporting and bound run history; migration 0130 fences uncertain Gmail delivery; `backend/ops/supabase_workers/` installs inactive platform Cron jobs. See `docs/SUPABASE_WORKER_MIGRATION.md` for staged handover. | +| Content lifecycle | `docs/CONTENT_ARCHITECTURE.md`, `docs/FUTURE_REFILL_ROLLOUT.md`; portable subject/curriculum imports, durable refill, reviewed publication, daily review, protected health report. `docs/CONTENT_OPERATIONS.md` preserves the earlier Python rollout. | +| Content engine | `supabase/functions/worker/` runs scheduled Gemini drafting, reminders and reconciliation; `backend/app/services/generation.py`, `pool.py`, `prefetch.py` retain Python maintenance and reference paths. | +| Operations | `.github/workflows/supabase-production-deploy.yml`, `.github/workflows/supabase-production-cron-install.yml`, `backend/ops/supabase_workers/`, `mobile/eas.json`, `mobile/app.config.js`. | +| Hosting history | `docs/RAILWAY_MIGRATION.md` and `docs/SUPABASE_WORKER_MIGRATION.md` retain factual cutover evidence. Current deployment and release instructions are in `RELEASING.md`, `docs/SCHEMA_VERIFICATION.md`, and `docs/INCIDENT_RESPONSE.md`. | +| Supabase scheduled workers | `supabase/functions/worker/` validates signed Cron calls; migrations 0119–0120 own the run ledger and HTTP reconciliation; migrations 0128–0129 feed owner reporting and bound run history; migration 0130 fences uncertain Gmail delivery; `backend/ops/supabase_workers/` installs platform Cron jobs. | | Documentation | Root `README.md`, `RELEASING.md`, `CONTRIBUTING.md`, `docs/ARCHITECTURE.md`, `docs/ROADMAP.md`, and the backend/mobile guides. | | Agent guidance | Root `AGENTS.md`; `mobile/AGENTS.md` adds Expo documentation requirements and `mobile/CLAUDE.md` references it. | | Authentication email | `backend/email-templates/` contains branded signup, recovery, and password-changed HTML; `docs/EMAIL_TEMPLATES.md` covers manual Supabase installation and activation checks. Templates use the configured sender and are not installed by app deployment. | @@ -54,8 +54,8 @@ gates. See [editorial-api.md](editorial-api.md) for permissions, retry semantics and rollout. The `admin/` website (#278) consumes these APIs; mobile attribution (#280) projects exact-version evidence through learner reads; owner reporting (#297) remains a separate child of #263. `services/editorial_notifications.py` drains the private #279 outbox through the -existing reminders worker; `editorial_mail.py` uses Gmail HTTPS/OAuth (no Railway -SMTP upgrade). `editorial_email_template.py` renders the escaped HTML template +scheduled reminders worker; `editorial_mail.py` uses Gmail HTTPS/OAuth. +`editorial_email_template.py` renders the escaped HTML template in `app/templates/editorial_review.html` alongside a plain-text fallback. `api/v1/editorial_notifications.py` exposes owner delivery/policy controls and the reviewer timezone. `admin/src/Notifications.tsx` shows overdue @@ -67,7 +67,7 @@ claim fencing, provider orchestration and private draft completion. Migration 0035 stores immutable source/feedback/result links and backlog claim tokens. `editorial_generation_status.py` exposes bounded private supply/planning status; `review_capacity.py` shares human-work limits across pool/prefetch/rewrite paths. -The existing `workers/pool_topup.py` runs a bounded revision batch before refill; +The Edge `worker` function runs a bounded revision batch before refill; `content_health.py` adds aggregate job conditions. See [editorial-generation.md](editorial-generation.md) for API and staging contracts. @@ -109,7 +109,7 @@ packages, diffs, history, comments, checklist decisions and safe corrections. `LegacyEnrichment.tsx` previews eligible existing lessons and controls one subject batch at a time. `services/legacy_generation.py` builds a grounded complete-card candidate and `legacy_enrichment_worker.py` stages it privately -through the scheduled `pool_topup.py` run; only a later human approval publishes. +through the scheduled Edge `legacy_enrichment` job; only a later human approval publishes. `MarkdownText.tsx` safely renders lesson writing, with `markdownCode.tsx` adding bounded Prism token highlighting to fenced code; `LessonEditor` shows a live preview. Identifiers and configuration fields remain plain text. @@ -158,8 +158,8 @@ recorded low-supply topic one normal slot before any optional urgent second pass response commits. Editorial corrections and legacy enrichment retain their own generation paths and cannot consume this allowance. `content_health.py` reports per-topic low-supply eligibility and the separate budget. See -[FUTURE_REFILL_ROLLOUT.md](FUTURE_REFILL_ROLLOUT.md) for release-only migration, -Railway variables, verification and rollback. +[FUTURE_REFILL_ROLLOUT.md](FUTURE_REFILL_ROLLOUT.md) for worker gates, +verification and rollback. ## Future multilingual content (#265) @@ -358,20 +358,19 @@ share `hooks/useRefreshControl.tsx` for native pull gestures and refresh buttons ## Incident response [INCIDENT_RESPONSE.md](INCIDENT_RESPONSE.md) defines the production triage and -communication sequence for Railway API/workers, Supabase, GitHub release gates, +communication sequence for the Supabase API/workers, GitHub release gates, and mobile delivery. It deliberately separates learner-safe UI recovery from private diagnostics and restore procedures. ## Backend request and service flow -The production Supabase API lives in `supabase/functions/api/` and +The production Supabase API from #416 lives in `supabase/functions/api/` and `backend/migrations/0046_supabase_api_basics.sql`–`0116_supabase_profile_avatar_api.sql`. All 73 JSON paths have candidate handlers, including private avatar upload, short-lived Storage signing, and editorial read/command routes. The avatar image module bundles a pinned ImageMagick WASM file for 512px JPEG conversion. -Its `public_pages.ts` serves the browser confirmation, recovery and learner -privacy pages at the same API path. The historical route checklist and parity -gates are in [SUPABASE_API_ROUTE_PARITY.md](SUPABASE_API_ROUTE_PARITY.md). +Production clients use the Edge route. The historical parity checklist is in +[SUPABASE_API_ROUTE_PARITY.md](SUPABASE_API_ROUTE_PARITY.md). `backend/tools/apply_staging_migrations.py` applies ordered migrations only to a fresh staging project after checking its project reference against the connection. Its `--inspect` mode reports staging schema metadata and profile row presence @@ -409,7 +408,7 @@ Authenticated routes pass through `core/rate_limit.py` after JWT verification. I | `me.py`: `PUT /v1/me/topics`, `PATCH /v1/me` | Whole-set follows, profile name, PostgreSQL-validated timezone. | | `me.py`: `GET/PUT /v1/me/notifications`, `POST/DELETE /v1/me/push-token` | Reminder preferences and scoped device registration/removal. | | `concepts.py`: `GET /v1/concepts/{slug}`, `PUT/DELETE .../like`, `.../save` | Published lesson detail and independent interaction writes. | -| `pages.py`: `GET /privacy`, `/confirmed`, `/reset-password` | Historical FastAPI equivalents of the production Edge `public_pages.ts` routes. Email redirects use the mobile API origin and recovery calls Supabase Auth in the browser. See [auth redirects](AUTH_REDIRECTS.md). | +| `pages.py`: `GET /privacy`, `/confirmed`, `/reset-password` | Public privacy and branded Auth landing pages; email redirects use the mobile API origin and reset uses Supabase Auth in the browser. See [auth redirects](AUTH_REDIRECTS.md). | `router.py` mounts authenticated feature routers under `/v1`. Response/input models live in `schemas/daily.py`, `me.py`, `notifications.py`, and `topics.py`. @@ -503,7 +502,7 @@ The contract records migration hashes and the candidate Edge API RPC definitions alongside the reviewed application schema. `workers/schema_check.py` performs the bounded read-only target check using `DIRECT_URL`; `docs/SCHEMA_VERIFICATION.md` covers contract maintenance and -Railway/GitHub setup. No SQL is applied by verification. +protected GitHub deployment. No SQL is applied by verification. `db/models.py` mirrors the SQL schema; migrations are the schema authority. The seventeen tables cover profiles, topics, concepts, user topics, daily assignments, @@ -563,8 +562,8 @@ and the session pooler. Applied migrations must not be rewritten. the deployed backend/worker SHA; its guard rejects missing/mismatched revisions and non-main dispatches. It publishes production then preview OTA, creates a version tag/GitHub release, and dispatches `release-apk.yml`. APK publication - is gated on native `runtimeVersion` changes. Railway deploys the backend - independently; follow `RELEASING.md` for migration and release ordering. + is gated on native `runtimeVersion` changes. The Supabase Edge API and worker + deploy through a separate protected workflow; follow `RELEASING.md` for ordering. - `pr-quality.yml` is the general pull-request gate for `develop` and `main`. It runs mobile Node 24 typechecking/tests and backend Ruff F/E9 plus pytest. The backend job installs Podman and fails if its disposable PostgreSQL 16 diff --git a/docs/FUTURE_REFILL_ROLLOUT.md b/docs/FUTURE_REFILL_ROLLOUT.md index 0deadaca..9fcc9bf3 100644 --- a/docs/FUTURE_REFILL_ROLLOUT.md +++ b/docs/FUTURE_REFILL_ROLLOUT.md @@ -1,105 +1,66 @@ -# Future curated-card refill rollout - -Issue #353 keeps future lesson refill separate from legacy enrichment and editorial -corrections. Every generated lesson remains a private draft until a reviewer -approves the exact version. - -The first reviewed launch plan is -[`backend/content/curriculum.refill-launch.json`](../backend/content/curriculum.refill-launch.json): -one distinct lesson for each existing subject, with a learning objective and -primary source. The production backend image contains this file. It is a -bounded starting backlog, not an infinite AI curriculum planner. Add further -reviewed plans as readers use these lessons. - -## Release procedure - -Migration `0043_future_refill_daily_usage.sql` is already recorded as applied -in production. Do not reapply or edit it. Enable future refill only after a -separate release and operator review of the existing-lesson backfill. - -1. Verify the deployed API and worker revisions match the release commit. -2. Verify the existing `future_refill_daily_usage` tables still have RLS enabled - and neither `anon` nor `authenticated` can read or write them. -3. Deploy the API and every generation-capable Railway worker with the same - variables. Keep `GEMINI_API_KEY` in Railway Variables only. -4. From the deployed backend image, import the reviewed launch plan once with - `python -m app.workers.content import-curriculum content/curriculum.refill-launch.json`. - The command is transactional and an exact re-import is safe. Inspect any - overlap warning against the live catalog before leaving the plans in the - queue. A duplicate or missing prerequisite is an import failure that needs - a corrected plan; never bypass catalog validation. - -## Initial Railway values - -Set these values consistently on the API, `pool-topup`/prefetch worker, and any -other service that runs future refill: - -```text -GEMINI_MODEL=gemini-3.1-flash-lite -GENERATION_ENABLED=false -FUTURE_REFILL_ENABLED=false -LEGACY_ENRICHMENT_ENABLED=false -CONTENT_LOW_WATERMARK=10 -CONTENT_CRITICAL_WATERMARK=3 -FUTURE_REFILL_DAILY_CALL_CAP=5 -FUTURE_REFILL_TOPIC_DAILY_CAP=1 -FUTURE_REFILL_URGENT_ENABLED=false -FUTURE_REFILL_URGENT_DAILY_CALL_CAP=10 -FUTURE_REFILL_URGENT_TOPIC_DAILY_CAP=2 -EDITORIAL_AUTO_CORRECTION_ENABLED=true -``` - -`GENERATION_ENABLED=false`, `FUTURE_REFILL_ENABLED=false`, and -`FUTURE_REFILL_DAILY_CALL_CAP=0` are independent -immediate stops. The future-refill worker creates one lesson per eligible topic -per normal day regardless of the older `CONTENT_GENERATION_BATCH` setting; do -not lower that general legacy/correction setting for this rollout. Do not enable -urgent mode during the first rollout. -`FUTURE_REFILL_ENABLED=false` also blocks owner requests for new planned drafts -while existing published lessons are being enriched; AI corrections and the -separately enabled legacy batch remain private review paths. - -After the launch plan is imported and the deployed worker is healthy, set -`GENERATION_ENABLED=true` and `FUTURE_REFILL_ENABLED=true` on the API and -`pool-topup` services, keeping the caps above. The scheduled worker checks -active reader demand and prepares at most one new private draft per low-supply -subject per normal day. It skips subjects with no plan or full review capacity. -The owner can verify the reason in content health and AI requests. Railway's -current `22:00 UTC` schedule corresponds to `03:00` in Pakistan; check the -actual schedule before promising that hour. Do not turn on urgent refill until -the first normal day has been inspected. - -A reviewer's **Request changes** decision queues one private AI correction -when generation is enabled. The job stays bound to that reviewer and is -cancelled if the membership is revoked before processing. The worker runs it -on its next scheduled or manually started pass; it does not edit or publish -the existing lesson. Set `EDITORIAL_AUTO_CORRECTION_ENABLED=false` to pause -new automatic correction requests while leaving existing queued jobs intact. - -## First-day verification - -After deployment, enable `GENERATION_ENABLED=true` and -`FUTURE_REFILL_ENABLED=true` for one day and use the protected operations -report to confirm: - -- no more than five future-refill calls were reserved; -- each low topic used no more than one call; -- only topics at or below ten unread published cards were eligible; -- generated lessons are drafts and wait for ordinary review; -- editorial corrections and legacy batch work do not affect the refill ledger. - -Approve one draft through the editorial workflow and verify it appears in the -learner app only after publication. If the provider rate-limits, review capacity -is full, or an allowance is exhausted, the worker logs a bounded reason and -stops without retrying on the HTTP request path. - -## Urgent mode and rollback - -Enable urgent mode only after reviewing a full day of quota and reviewer-load -evidence. It allows a critical topic (three unread cards or fewer) a second call -that day, under the separate ten-call global ceiling. - -To pause safely, set `FUTURE_REFILL_ENABLED=false`, `GENERATION_ENABLED=false`, or -`FUTURE_REFILL_DAILY_CALL_CAP=0` on every generation-capable Railway service and -redeploy or restart them. Existing published lessons and private drafts remain -unchanged. +# Future curated-card refill + +Issue #353 keeps new lesson generation separate from legacy enrichment and +editorial corrections. Gemini drafts from a human-curated title plan; it does +not choose the curriculum or publish to learners. Every generated lesson stays +private until a reviewer approves and publishes its exact revision. + +The original five-subject starting plan is +[`backend/content/curriculum.refill-launch.json`](../backend/content/curriculum.refill-launch.json). +It is a bounded historical seed, not an infinite planner. Import further +reviewed titles as readers consume lessons. An empty pending plan cannot be +fixed by increasing a model quota or turning on a scheduler. + +## Before enabling production refill + +1. Confirm the production API and Edge worker were deployed from the intended + `main` commit through the protected workflow in [RELEASING.md](../RELEASING.md). + Verify migration `0043_future_refill_daily_usage.sql` and later worker + migrations against the actual production schema. Do not edit applied SQL. +2. Review the current catalog, private drafts and pending revisions. Publish + only exact lesson versions that pass the full editorial checklist. A full + review queue blocks new claims by design. +3. Inspect failed planned titles individually. Correct duplicates, content + validation problems or provider configuration before granting an audited + retry; never reset all failed rows. See [content operations](CONTENT_OPERATIONS.md). +4. Import a reviewed plan with distinct slugs, objectives, prerequisites and + sources. The Python maintainer CLI can import it using the intended Supabase + database connection, but verify the project ref and a dry-run plan first. + Exact re-import is safe; an overlap warning still needs human inspection. +5. Verify the production Edge worker has its private Gemini key and model, + bounded call caps, and the `content_pool` Cron job. Keep `FUTURE_REFILL_ENABLED` + false until the plan and reviewer capacity are ready. Staging and production + project refs and secrets must stay separate. + +## Controlled activation + +The production Edge worker reads `GENERATION_ENABLED` and +`FUTURE_REFILL_ENABLED` independently. Both must be true for curated refill. +Enable the `content_pool` row in `edge_worker_jobs` and its matching Cron job +only after checking the exact production project and target worker URL. The +normal path claims at most one private draft per eligible low-supply topic per +day, bounded by `FUTURE_REFILL_DAILY_CALL_CAP=5` and +`FUTURE_REFILL_TOPIC_DAILY_CAP=1`. Leave urgent refill disabled on the first day. +The worker skips topics with no pending plan, a full review queue, no active +reader demand, or exhausted allowance. Learner HTTP requests do not wait for a +model call. + +Reviewer's **Request changes** can queue a separate private AI correction when +`EDITORIAL_AUTO_CORRECTION_ENABLED=true`. It does not publish a replacement. +Legacy enrichment uses its own job and review path. Do not infer that either +worker creates new curriculum titles. + +## First-day verification and pause + +Inspect `cron.job`, `edge_worker_jobs`, and the latest terminal +`edge_worker_runs` for `content_pool`. Confirm `status=succeeded`, +`disabled=0`, and a valid HTTP receipt. Check the plan, draft, failure and +`future_refill_daily_usage` counts. No more than five normal calls should be +reserved globally and one per eligible topic. A generated draft must appear +in the review queue and become available to learners only after publication. + +To pause new work, set `FUTURE_REFILL_ENABLED=false` or the daily call cap to +zero, and turn off the `content_pool` Cron and worker gates if needed. Existing +published lessons, private drafts and review evidence remain intact. Recheck +the latest worker run rather than treating a successful dispatch as proof that +a card was generated or published. diff --git a/docs/PROFILE_AVATARS.md b/docs/PROFILE_AVATARS.md index 4b4cc284..ae19fbae 100644 --- a/docs/PROFILE_AVATARS.md +++ b/docs/PROFILE_AVATARS.md @@ -18,6 +18,6 @@ Before merging and deploying the feature: 1. Apply migration `0040_profile_avatar_bio.sql` through the normal migration process. Do not add it to `backend/migrations/applied.txt` until production confirms it. 2. In Supabase Storage, create `profile-avatars` as a **private** bucket with a 256 KiB file limit and JPEG/PNG/WebP allowlist. Storage buckets are managed by Supabase Storage, not the application PostgreSQL migration runner. -3. Confirm Railway has `SUPABASE_SERVICE_ROLE_KEY`; it remains backend-only and must never be copied to Expo variables. +3. Confirm the production Supabase Edge API has its server-only Storage credential; it must never be copied to Expo variables. 4. Build and install a new Android/iOS binary. `expo-image-picker` and its camera/photo permission messages are native configuration, so an OTA alone cannot enable photo selection on existing installs. 5. On a staging account, test each preset, denied library/camera permission, picker cancellation, preview Save/Cancel, an oversized/invalid image, photo replacement/removal, sign-out during picker or upload, and public-avatar sharing on/off from another browser. Check the Android activity-recreation path on a physical device; web tests do not prove native picker behavior. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 334e3519..8f0150a4 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -32,7 +32,7 @@ Development moves in small, complete phases. Shipped phases are checked. ## Phase 6 — Deployment ✅ -- [x] Backend on Railway (auto-deploys from `main`) +- [x] Production API and scheduled workers on Supabase Edge Functions - [x] EAS Build (APK) and EAS Update (OTA on merge) - [x] GitHub Actions automation diff --git a/docs/owner-dashboard.md b/docs/owner-dashboard.md index 457acbd4..d14516b4 100644 --- a/docs/owner-dashboard.md +++ b/docs/owner-dashboard.md @@ -67,29 +67,25 @@ not a team-wide unique count when several people reviewed the same concept. API/database “reachable” means this authorized reporting request succeeded; it is not historical uptime. If the API or database is unavailable the UI shows a report error. Generation/email/telemetry switches and the configured call cap -describe the serving API process, not the separately configured workers. Check -worker settings in their service consoles. Reserved daily generation calls, AI -job states and email-batch states come from shared database records. Generation budgets +describe the serving API process, not the separately configured Edge worker. +Check production Edge secrets and worker gates separately. Reserved daily +generation calls, AI job states and email-batch states come from shared database +records. Generation budgets retain the existing `America/Los_Angeles` day. A reservation can include a failed attempt and is not a billing total. Email “Accepted” means the provider accepted it; it does not prove inbox delivery. -Optional `OWNER_TELEMETRY_ENABLED` defaults to **false**. When enabled on API, -reminders and pool-topup, the existing processes record only allowlisted status -codes, UTC timestamp, service, severity and a random correlation UUID in -`owner_operation_events`. API failure correlation matches its public incident ID -(compact and hyphenated UUIDs are accepted by the event filter). Worker start -and terminal events share an ID. -Telemetry never includes credentials, stack traces, raw errors or user input. -It uses a separate short transaction, 750 ms statement timeout and a one-second -write deadline; a logging failure does not replace the worker's result. - -- Worker observations are unavailable when absent. The API’s own telemetry switch - does not suppress observations written by workers. Disabling a worker’s telemetry - stops new observations; existing ones age into stale status. Starts without a - terminal observation become stale after 30 minutes. Terminal observations are - stale after 45 minutes for the 15-minute reminders cron and 36 hours for the - daily pool-topup cron. If schedules change, adjust these thresholds in code. +Optional legacy Python `OWNER_TELEMETRY_ENABLED` defaults to **false**. When +used in a controlled local process, it records allowlisted status codes, UTC +timestamps, service, severity and a random correlation UUID in +`owner_operation_events`. Production scheduled-job observations come from the +separate `edge_worker_runs` ledger. Neither feed includes credentials, stack +traces, raw errors or user input. + +- Production worker status comes from `edge_worker_jobs` and + `edge_worker_runs`. A missing run is unavailable, a disabled job is disabled, + and a completed dispatch can become stale after its job-specific interval. + Check the current Cron schedule before interpreting a stale result. - “Completed” describes process completion, not generation, publication or email success. Individual skipped/failed work remains in the existing job/outbox counters and provider consoles. @@ -152,10 +148,9 @@ When #281 performs the rollout after the production release: unnecessary. 4. Confirm the intended owner account has an approved name, MFA and `manage_reviewers`. An ordinary reviewer must be denied all four report URLs. -5. Optionally enable `OWNER_TELEMETRY_ENABLED=true` on the API, reminders and - pool-topup services once the migration is verified. Wait for ordinary scheduled - runs; do not run content generation just to paint a green status. Until then, - worker observations correctly remain unavailable. +5. Check the enabled Supabase worker gates and ordinary scheduled runs. Do not + run content generation just to paint a green status. When a job is disabled, + its observation should accurately reflect that state. 6. Reconcile a controlled learner completion and a known review/publication with the reports. Verify sign-out, revocation, unavailable states, narrow layout, keyboard use and both themes on the deployed origin. Verify actual host headers From b15bb4d20f7e2dda647903519fbedeace625e58c Mon Sep 17 00:00:00 2001 From: Muawiya Amir Date: Wed, 28 Oct 2026 17:22:13 +0500 Subject: [PATCH 10/17] docs: mark former deployment runbooks as historical --- docs/CONTENT_OPERATIONS.md | 6 ++++++ docs/EDITORIAL_ROLLOUT.md | 5 +++++ docs/LEGACY_ENRICHMENT_ROLLOUT.md | 4 ++++ docs/RAILWAY_MIGRATION.md | 4 ++++ docs/SUPABASE_API_STAGING.md | 3 +++ docs/SUPABASE_WORKER_MIGRATION.md | 4 ++++ docs/handbook/ONE_CONCEPT_HANDBOOK.md | 5 +++++ docs/weekly-quiz-notifications.md | 3 +++ 8 files changed, 34 insertions(+) diff --git a/docs/CONTENT_OPERATIONS.md b/docs/CONTENT_OPERATIONS.md index 3341ed3c..6f2fb38a 100644 --- a/docs/CONTENT_OPERATIONS.md +++ b/docs/CONTENT_OPERATIONS.md @@ -1,5 +1,11 @@ # Content operations runbook +> Historical #195 rollout instructions for the Python backend. For current +> Supabase Edge deployment and worker gates, use [RELEASING.md](../RELEASING.md), +> [INCIDENT_RESPONSE.md](INCIDENT_RESPONSE.md), and +> [FUTURE_REFILL_ROLLOUT.md](FUTURE_REFILL_ROLLOUT.md). Do not run the old +> deployment or schedule steps below against production. + This runbook implements [the sustainable learning architecture](CONTENT_ARCHITECTURE.md) for #195. The repository owner is the initial content operator and reviewer. Broader crash reporting and application observability remain in #161. diff --git a/docs/EDITORIAL_ROLLOUT.md b/docs/EDITORIAL_ROLLOUT.md index 1ee19512..cfe1303b 100644 --- a/docs/EDITORIAL_ROLLOUT.md +++ b/docs/EDITORIAL_ROLLOUT.md @@ -1,5 +1,10 @@ # Reviewer website: direct production rollout (#281 / #313) +> Historical rollout record. Current Netlify and Supabase Edge setup is in +> [admin/README.md](../admin/README.md); use +> [RELEASING.md](../RELEASING.md) for protected production deployment. +> The former service URLs and instructions below are not current. + The reviewer and owner dashboards are two sections of the same `admin/` site. They use the existing production FastAPI service and Supabase Auth; no new backend service or paid frontend plan is required. Static hosting needs no diff --git a/docs/LEGACY_ENRICHMENT_ROLLOUT.md b/docs/LEGACY_ENRICHMENT_ROLLOUT.md index 14983749..6d3c4275 100644 --- a/docs/LEGACY_ENRICHMENT_ROLLOUT.md +++ b/docs/LEGACY_ENRICHMENT_ROLLOUT.md @@ -1,5 +1,9 @@ # Complete existing lessons, one subject at a time +> Historical rollout record. The current scheduled implementation is the +> Supabase Edge `legacy_enrichment` worker; use [RELEASING.md](../RELEASING.md) +> and [INCIDENT_RESPONSE.md](INCIDENT_RESPONSE.md) for deployment and diagnosis. + Migration `0042_legacy_enrichment_batches.sql` is already recorded as applied in production. Do not reapply or edit it. The new worker and owner controls use those existing tables. This change needs a normal `develop` → `main` release diff --git a/docs/RAILWAY_MIGRATION.md b/docs/RAILWAY_MIGRATION.md index f255903f..37149479 100644 --- a/docs/RAILWAY_MIGRATION.md +++ b/docs/RAILWAY_MIGRATION.md @@ -1,5 +1,9 @@ # Railway account migration +> Historical cutover record only. These services were retired in October 2026. +> Current deployment and recovery steps are in [RELEASING.md](../RELEASING.md) +> and [INCIDENT_RESPONSE.md](INCIDENT_RESPONSE.md). + Runbook for [issue #230](https://github.com/Coding-Moves/one-concept/issues/230). This moves hosting while retaining the existing Supabase database, Auth project, Expo project, Android package/signing credentials, and application behavior. diff --git a/docs/SUPABASE_API_STAGING.md b/docs/SUPABASE_API_STAGING.md index 3ff06704..fa711670 100644 --- a/docs/SUPABASE_API_STAGING.md +++ b/docs/SUPABASE_API_STAGING.md @@ -1,5 +1,8 @@ # Supabase API candidate staging (#416) +> Historical staging validation record. Production now uses the Supabase Edge +> API. Current release steps are in [RELEASING.md](../RELEASING.md). + The current candidate implements all 73 inventoried JSON paths under `/functions/v1/api`. [The route matrix](SUPABASE_API_ROUTE_PARITY.md) records each consumer and validation state. Local route coverage is complete; hosted diff --git a/docs/SUPABASE_WORKER_MIGRATION.md b/docs/SUPABASE_WORKER_MIGRATION.md index ca6ed37d..49bce796 100644 --- a/docs/SUPABASE_WORKER_MIGRATION.md +++ b/docs/SUPABASE_WORKER_MIGRATION.md @@ -1,5 +1,9 @@ # Supabase worker migration (#417) +> Historical handover evidence. Production now uses the Supabase Edge worker; +> use [RELEASING.md](../RELEASING.md) and +> [INCIDENT_RESPONSE.md](INCIDENT_RESPONSE.md) for current operations. + This is the implementation and handover record for the second Railway exit PR. It records verified live settings separately from source defaults. Keep provider credentials, token values, URLs containing secrets, and recipient addresses out diff --git a/docs/handbook/ONE_CONCEPT_HANDBOOK.md b/docs/handbook/ONE_CONCEPT_HANDBOOK.md index e5f06825..3fc60ecd 100644 --- a/docs/handbook/ONE_CONCEPT_HANDBOOK.md +++ b/docs/handbook/ONE_CONCEPT_HANDBOOK.md @@ -1,5 +1,10 @@ # 01 | Your app in twelve answers +> Historical September 2026 snapshot. Hosting and release details below record +> the former deployment. Current production operations are in +> [RELEASING.md](../../RELEASING.md) and +> [INCIDENT_RESPONSE.md](../INCIDENT_RESPONSE.md). + One Concept is a daily microlearning app: a signed-in learner receives one short technical concept, reads its explanation and example, and can mark it learned, like it, save it, or share it. Topic follows shape future assignments. The backend is the authority for assignments and learning progress. | Your question | What the inspected code actually does | diff --git a/docs/weekly-quiz-notifications.md b/docs/weekly-quiz-notifications.md index bf6e616e..32e626cd 100644 --- a/docs/weekly-quiz-notifications.md +++ b/docs/weekly-quiz-notifications.md @@ -1,5 +1,8 @@ # Weekly quiz availability notifications +> Historical feature rollout. Current scheduling is controlled by the Supabase +> Edge worker gates; see [INCIDENT_RESPONSE.md](INCIDENT_RESPONSE.md). + Issue [#268](https://github.com/Coding-Moves/one-concept/issues/268) adds optional push alerts using the existing Expo registration and Railway reminders worker. No additional paid service, API key, native dependency or background mobile task From 9d8c4d3f9c5099c3b651fed1b9b9af820159342e Mon Sep 17 00:00:00 2001 From: Muawiya Amir Date: Wed, 28 Oct 2026 17:22:38 +0500 Subject: [PATCH 11/17] docs: direct readers from old feature plans to current worker guides --- docs/SUPABASE_API_ROUTE_PARITY.md | 4 ++++ docs/editorial-generation.md | 3 +++ docs/editorial-notifications.md | 4 ++++ 3 files changed, 11 insertions(+) diff --git a/docs/SUPABASE_API_ROUTE_PARITY.md b/docs/SUPABASE_API_ROUTE_PARITY.md index 32e5310f..feb14680 100644 --- a/docs/SUPABASE_API_ROUTE_PARITY.md +++ b/docs/SUPABASE_API_ROUTE_PARITY.md @@ -1,5 +1,9 @@ # Supabase API route parity for #416 +> Historical migration checklist. Production clients now use the Supabase +> Edge API. Current operations are in [RELEASING.md](../RELEASING.md) and +> [INCIDENT_RESPONSE.md](INCIDENT_RESPONSE.md). + Source: current `develop` FastAPI OpenAPI plus four routes excluded from its schema. The target is `/functions/v1/api` with the current `/v1/...` path appended. This is an inventory, not evidence that a candidate route works. The mobile client calls learner endpoints; the owner/reviewer site calls `/v1/editorial/...`; public visitors use profile JSON and `/p/{token}`. Every `/v1/...` route requires a verified token except public profiles. Editorial routes apply their existing per-route capability and MFA policy; `/v1/editorial/me` can report that MFA is required. The HTML routes belong to #418 because the default Edge URL does not serve HTML as HTML. diff --git a/docs/editorial-generation.md b/docs/editorial-generation.md index e630f242..c7ebff30 100644 --- a/docs/editorial-generation.md +++ b/docs/editorial-generation.md @@ -1,5 +1,8 @@ # Bounded editorial generation (#277) +> Original feature design and rollout record. For current Supabase Edge refill +> gates and plan capacity, use [FUTURE_REFILL_ROLLOUT.md](FUTURE_REFILL_ROLLOUT.md). + Step 4 of [#263](https://github.com/Coding-Moves/one-concept/issues/263) extends existing Gemini workers. New lessons and AI corrections stay private until the exact new revision passes authenticated human approval and publication. A low diff --git a/docs/editorial-notifications.md b/docs/editorial-notifications.md index 05af1364..82753684 100644 --- a/docs/editorial-notifications.md +++ b/docs/editorial-notifications.md @@ -1,5 +1,9 @@ # Reviewer email notifications (#279) +> Historical feature rollout and October 2026 cost research. The production +> sender now runs in the Supabase Edge worker. For current gates and incident +> checks, use [INCIDENT_RESPONSE.md](INCIDENT_RESPONSE.md) and the owner dashboard. + Assigned reviewers receive batched links to lessons that need review. The queue remains the source of truth, including when email is disabled or unavailable. No email approves or publishes content. This is phase 6 of #263; deployment and From 10262149c85724970d5ff31c163b2f327b6d265b Mon Sep 17 00:00:00 2001 From: Muawiya Amir Date: Wed, 28 Oct 2026 17:23:01 +0500 Subject: [PATCH 12/17] docs: record retired hosting cleanup and verification --- docs/WORK_LOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/WORK_LOG.md b/docs/WORK_LOG.md index 23f0126a..0af10a83 100644 --- a/docs/WORK_LOG.md +++ b/docs/WORK_LOG.md @@ -7,6 +7,12 @@ claims as completed work. ## Current status +### Remove retired hosting configuration — 2026-10-28 + +- The owner requested a repository-wide cleanup after deleting the former hosted services. This work removes live deployment configuration and stale runtime, release, website and email guidance. It is added to open [PR #459](https://github.com/Coding-Moves/one-concept/pull/459), which also holds the three Supabase Auth landing pages. The narrow production hotfix remains open in PR #460; neither PR is merged or deployed here. +- The Supabase Auth email templates already contain no former host URL. Applied SQL migrations, generated schema contracts and factual cutover history remain intact. Obsolete runbooks now display historical notices; current guides point to the Supabase Edge API and scheduled workers. The review site's public privacy link now targets the Supabase Edge privacy page, and its build validates the exact Edge API URL and matching Auth project. +- Validation: all 22 review-site unit tests passed; its TypeScript check and production build passed with public test values. A repository search found no former host string in active runtime code, email templates, current setup guides or workflows; `git diff --check` passed. This is source and build verification, not proof that the pending public pages have been deployed. + ### Production Supabase cutover landing pages and content supply — 2026-10-25 - The owner deleted the Railway services after confirming the production app, review site and real 08:00 reminder worked on Supabase. Production SQL showed `daily_reminders`, `editorial_generation`, `legacy_enrichment`, and `http-reconcile` active; `content_pool` and `prefetch` remain off. For Software Engineering, the owner's aggregate query showed 26 published lessons, 2 draft concepts, 20 pending-review revisions, zero pending curated titles and 6 failed titles. Some pending revisions may correct published lessons, so they are not 20 new lessons. Keep refill paused while the owner reviews exact content and failed-title causes. From 4002946707fb143ffbdbc2516beb59d1dca654a9 Mon Sep 17 00:00:00 2001 From: Muawiya Amir Date: Wed, 28 Oct 2026 17:24:12 +0500 Subject: [PATCH 13/17] docs: map public Supabase auth pages --- docs/CODEBASE_MAP.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/CODEBASE_MAP.md b/docs/CODEBASE_MAP.md index d7867436..3b3f2033 100644 --- a/docs/CODEBASE_MAP.md +++ b/docs/CODEBASE_MAP.md @@ -369,6 +369,8 @@ The production Supabase API from #416 lives in `supabase/functions/api/` and All 73 JSON paths have candidate handlers, including private avatar upload, short-lived Storage signing, and editorial read/command routes. The avatar image module bundles a pinned ImageMagick WASM file for 512px JPEG conversion. +Its `public_pages.ts` serves the browser confirmation, recovery, and learner +privacy pages at the same API path. Production clients use the Edge route. The historical parity checklist is in [SUPABASE_API_ROUTE_PARITY.md](SUPABASE_API_ROUTE_PARITY.md). `backend/tools/apply_staging_migrations.py` applies ordered migrations only to From 4bfa0a99edccc906994c54ddbe62468440fd9044 Mon Sep 17 00:00:00 2001 From: Muawiya Amir Date: Wed, 28 Oct 2026 17:25:03 +0500 Subject: [PATCH 14/17] docs: remove retired host mention from public page source --- supabase/functions/api/public_pages.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/supabase/functions/api/public_pages.ts b/supabase/functions/api/public_pages.ts index 0edaf90c..e9e46efd 100644 --- a/supabase/functions/api/public_pages.ts +++ b/supabase/functions/api/public_pages.ts @@ -1,4 +1,4 @@ -/** Browser landing pages retained when the learner API moved off Railway. */ +/** Browser landing pages served beside the Supabase learner API. */ type PageConfig = { supabaseUrl: string; anonKey: string }; From 0b1d3af7b9efffd55d0f9b6a1afe822cb3081d3c Mon Sep 17 00:00:00 2001 From: Muawiya Amir Date: Wed, 28 Oct 2026 17:35:23 +0500 Subject: [PATCH 15/17] fix(auth): avoid false confirmation on direct page visits --- supabase/functions/api/handler.test.mjs | 36 +++++++++++++++++++++++++ supabase/functions/api/public_pages.ts | 8 ++++-- 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/supabase/functions/api/handler.test.mjs b/supabase/functions/api/handler.test.mjs index 394b2e5f..27b2a295 100644 --- a/supabase/functions/api/handler.test.mjs +++ b/supabase/functions/api/handler.test.mjs @@ -1,6 +1,7 @@ import test from 'node:test'; import assert from 'node:assert/strict'; import { createHash } from 'node:crypto'; +import { runInNewContext } from 'node:vm'; import { createHandler } from './handler.ts'; import './avatar.test.mjs'; @@ -40,6 +41,41 @@ test('public cutover pages serve browser HTML without leaking service credential assert.equal(rejected.headers.get('allow'), 'GET'); }); +test('confirmation page claims success only after an Auth redirect', async () => { + const handler = createHandler(config); + const body = await (await handler(request('/confirmed'))).text(); + const script = body.match(/`; From c70db691ebef31185cc2ad7412567ca3b70fa08c Mon Sep 17 00:00:00 2001 From: Muawiya Amir Date: Wed, 28 Oct 2026 17:36:01 +0500 Subject: [PATCH 16/17] docs: clarify production Edge routing and legacy Python paths --- backend/README.md | 7 ++++--- docs/CODEBASE_MAP.md | 8 +++++--- mobile/src/lib/supabase.ts | 6 +++--- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/backend/README.md b/backend/README.md index 1ba593dc..72eaefde 100644 --- a/backend/README.md +++ b/backend/README.md @@ -16,12 +16,13 @@ production reminders and generation run through the Supabase Edge worker. ## Sustainable content lifecycle See [the architecture](../docs/CONTENT_ARCHITECTURE.md) and -[the operator runbook](../docs/CONTENT_OPERATIONS.md). Content now uses durable +[the current refill guide](../docs/FUTURE_REFILL_ROLLOUT.md). Content now uses durable reader-aware supply targets, structured curriculum imports, explicit reviewed publication, and separate daily reviews when new lessons are exhausted. Run `python -m app.workers.content --help` for maintainer operations. -Migrations 0011–0015 must precede this backend; keep old generators disabled -during rollout. Daily review clients opt into `reviews=true` on `/v1/me/state` +The original #195 rollout applied migrations 0011–0015 before enabling these +rules. Current production schema changes follow [RELEASING.md](../RELEASING.md). +Daily review clients opt into `reviews=true` on `/v1/me/state` and complete an identified activity through `/v1/reviews/{id}/complete`. ## Layout diff --git a/docs/CODEBASE_MAP.md b/docs/CODEBASE_MAP.md index 3b3f2033..a00d3b21 100644 --- a/docs/CODEBASE_MAP.md +++ b/docs/CODEBASE_MAP.md @@ -385,8 +385,9 @@ For the initialized staging project, `--apply-incremental` requires the verified The protected incremental tag runs this path separately from the fresh-schema and Edge deploy jobs. It reloads PostgREST's RPC schema cache after verified SQL; `--reload-cache` is the guarded staging-only repair path for stale cache. -Migration #417 will move scheduled workers; #418 owns browser pages and client -cutover. Until then, the FastAPI implementation below remains authoritative. +The Edge API and worker own production traffic. The Python implementation below +remains a local reference and maintenance path; its routes and workers should +not be mistaken for the deployed production entry points. `main.py` configures CORS, routes, production documentation visibility, safe SQLAlchemy and unexpected-exception responses, and a shared JWKS cache, and engine cleanup. Its lifespan owns `db/keepalive.py`'s configurable @@ -458,7 +459,8 @@ models live in `schemas/daily.py`, `me.py`, `notifications.py`, and `topics.py`. - `services/reminders.py` claims due user/day/time slots before sending Expo push batches, handles timezone and midnight windows, suppresses completed days, and drops unregistered device tokens. A claimed but failed send can miss a reminder. -- `workers/pool_topup.py` and `workers/reminders.py` are cron entry points. +- `workers/pool_topup.py` and `workers/reminders.py` are retained Python + maintenance entry points. Production schedules call the Supabase Edge worker. `workers/rewrite_catalog.py` is a maintenance command that rewrites existing lessons through Gemini with the shared budget and generation kill switch; do not run it merely to inspect the project. diff --git a/mobile/src/lib/supabase.ts b/mobile/src/lib/supabase.ts index f10579df..368130bc 100644 --- a/mobile/src/lib/supabase.ts +++ b/mobile/src/lib/supabase.ts @@ -31,9 +31,9 @@ export async function readCachedSession(): Promise { /** * Supabase client, used only for authentication. * - * All application data goes through the FastAPI backend — the app never reads - * or writes tables directly, so this client's job is to obtain and refresh the - * access token that the backend verifies. + * All application data goes through the Supabase Edge API. The app never reads + * or writes tables directly; this client's job is to obtain and refresh the + * access token that the Edge API verifies. */ export const supabase = createClient(clientUrl, clientAnonKey, { global: { fetch: fetchWithTimeout }, From 1d21b821336066fb29ef9ec8fb42d9950355b348 Mon Sep 17 00:00:00 2001 From: Muawiya Amir Date: Wed, 28 Oct 2026 17:36:39 +0500 Subject: [PATCH 17/17] docs: record final auth landing review --- docs/WORK_LOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/WORK_LOG.md b/docs/WORK_LOG.md index 0af10a83..95e132c0 100644 --- a/docs/WORK_LOG.md +++ b/docs/WORK_LOG.md @@ -12,6 +12,7 @@ claims as completed work. - The owner requested a repository-wide cleanup after deleting the former hosted services. This work removes live deployment configuration and stale runtime, release, website and email guidance. It is added to open [PR #459](https://github.com/Coding-Moves/one-concept/pull/459), which also holds the three Supabase Auth landing pages. The narrow production hotfix remains open in PR #460; neither PR is merged or deployed here. - The Supabase Auth email templates already contain no former host URL. Applied SQL migrations, generated schema contracts and factual cutover history remain intact. Obsolete runbooks now display historical notices; current guides point to the Supabase Edge API and scheduled workers. The review site's public privacy link now targets the Supabase Edge privacy page, and its build validates the exact Edge API URL and matching Auth project. - Validation: all 22 review-site unit tests passed; its TypeScript check and production build passed with public test values. A repository search found no former host string in active runtime code, email templates, current setup guides or workflows; `git diff --check` passed. This is source and build verification, not proof that the pending public pages have been deployed. +- Final PR review found a direct visit to `/confirmed` falsely claimed email verification. Commit `0b1d3af` now shows a neutral message unless Supabase returns an access token, with script-level checks for direct, verified and recovery redirects. Commit `c70db69` corrects remaining production-versus-Python reference wording. The Supabase TypeScript check and all 95 API handler tests passed; hosted PR checks still need to rerun on the new head before merge. No production deployment was made. ### Production Supabase cutover landing pages and content supply — 2026-10-25