Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
446b7a1
fix(edge): restore public auth and privacy landing pages
Muawiya-contact Oct 25, 2026
024472c
fix(clients): point privacy links at Supabase API
Muawiya-contact Oct 25, 2026
dbc9748
docs: replace Railway auth redirect runbook for Supabase
Muawiya-contact Oct 25, 2026
baf6e4d
docs(admin): describe Supabase hosting in review privacy policy
Muawiya-contact Oct 25, 2026
95280c0
chore: remove retired hosting deployment configuration
Muawiya-contact Oct 28, 2026
b863b34
docs: make protected Supabase deploy the release path
Muawiya-contact Oct 28, 2026
5a5dda6
docs: describe Supabase API and worker operations
Muawiya-contact Oct 28, 2026
b0f738f
fix: point review site privacy and API config to Supabase
Muawiya-contact Oct 28, 2026
17651c1
docs: update active setup and content guides for Supabase Edge
Muawiya-contact Oct 28, 2026
b15bb4d
docs: mark former deployment runbooks as historical
Muawiya-contact Oct 28, 2026
9d8c4d3
docs: direct readers from old feature plans to current worker guides
Muawiya-contact Oct 28, 2026
1026214
docs: record retired hosting cleanup and verification
Muawiya-contact Oct 28, 2026
4002946
docs: map public Supabase auth pages
Muawiya-contact Oct 28, 2026
4bfa0a9
docs: remove retired host mention from public page source
Muawiya-contact Oct 28, 2026
0b1d3af
fix(auth): avoid false confirmation on direct page visits
Muawiya-contact Oct 28, 2026
c70db69
docs: clarify production Edge routing and legacy Python paths
Muawiya-contact Oct 28, 2026
1d21b82
docs: record final auth landing review
Muawiya-contact Oct 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/scripts/check-release-revision.sh
Original file line number Diff line number Diff line change
@@ -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
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -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:
Expand Down
69 changes: 38 additions & 31 deletions RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand All @@ -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.
Expand All @@ -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
Expand All @@ -60,26 +64,29 @@ 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.

## After a JS-only release
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 `<api-domain>/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.
Expand All @@ -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.
38 changes: 22 additions & 16 deletions admin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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

Expand Down
4 changes: 2 additions & 2 deletions admin/public/privacy.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
<p class="eyebrow">Privacy policy</p>
<h1>How we handle review workspace data</h1>
<p class="updated">Effective October 4, 2026</p>
<p>One Concept Review is a private editorial workspace operated by Coding Moves. This policy describes its accounts, review records, and optional email notifications. The <a href="https://one-concept-production.up.railway.app/privacy">One Concept learning app privacy policy</a> covers learner accounts and app data.</p>
<p>One Concept Review is a private editorial workspace operated by Coding Moves. This policy describes its accounts, review records, and optional email notifications. The <a href="https://egdszjoesxlccauahmsx.supabase.co/functions/v1/api/privacy">One Concept learning app privacy policy</a> covers learner accounts and app data.</p>

<h2>Editorial accounts and work</h2>
<p>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.</p>
Expand All @@ -23,7 +23,7 @@ <h2>Reviewer notification email</h2>
<p>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.</p>

<h2>Hosting, use, and retention</h2>
<p>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.</p>
<p>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.</p>
<p>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.</p>

<h2>Security and contact</h2>
Expand Down
9 changes: 9 additions & 0 deletions admin/src/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
20 changes: 15 additions & 5 deletions admin/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,13 @@ export function readConfig(env: Record<string, string | undefined>) {
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_")) {
Expand All @@ -26,9 +29,16 @@ export function readConfig(env: Record<string, string | undefined>) {
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",
};
Expand Down
3 changes: 3 additions & 0 deletions admin/src/hosting.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;");
});
});
Loading
Loading