Compare commits

Author SHA1 Message Date
jjbruntonandClaude Opus 5 3a019e62d3 fix(api): allow the admin console origin through CORS
The production allowlist held only https://sauci.app, so every
/v1/admin/* preflight from https://manage.sauci.app returned 204 with no
Access-Control-Allow-Origin and the browser blocked the request. Login
then reported "Access denied. You are not an administrator." because the
admin app treats any /v1/admin/me failure as a permissions failure.

Add the admin console origin and record in the README that new admin
hosts belong in the Compose contract rather than a Dokploy override.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 18:17:26 +01:00
jjbrunton 84712a950e Merge pull request #26 from jjbrunton/hotfix/admin-docker-context
fix(admin): restore admin docker build context, allow hotfix branches into main
2026-08-31 18:08:31 +01:00
jjbruntonandClaude Opus 5 d14869d745 ci(branch-policy): allow hotfix and release branches into main
The policy accepted only `staging` as a head for main, so any change
production needed before the next promotion had nowhere to go: the PR was
unmergeable by CI and branch protection blocks direct pushes.

Accept `hotfix/*` and `release/*` heads as well. The naming requirement
keeps a direct-to-main change deliberate and visible, and the master
freeze plus the "features target staging" rule are unchanged. Document
the escape hatch and the need to land the same commit on staging.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 18:03:48 +01:00
jjbruntonandClaude Opus 5 f1573f4906 fix(admin): keep admin app in the docker build context
The root .dockerignore excluded apps/admin, so the monorepo build context
dropped that workspace entirely: npm ci skipped it and
`npm run build -w @sauci/admin` failed with "No workspaces found".
The ignore file was authored for the mcp image and later extended with
re-includes for web; admin never got the same treatment.

Re-include apps/admin, and re-exclude node_modules/dist last so the
re-includes cannot pull host build artefacts into any image context.

Verified with `docker build -f apps/admin/Dockerfile .`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 18:03:48 +01:00
jjbrunton 83df3b45f8 Merge pull request #23 from jjbrunton/staging
Release v1.0.8: Expo SDK 56 and EAS Observe
2026-08-31 15:47:29 +01:00
jjbruntonandClaude Fable 5 9609a9084b fix(deps): pin @react-native/metro-config to end npm 10/11 lock divergence
react-native-worklets depends on @react-native/metro-config with a
wildcard range; npm 11 (local) resolves and hoists 0.87.x where npm 10
(CI runners and Docker images) requires 0.85.3 at the same position, so
every lockfile regeneration broke one side. Pin 0.85.3 via the root
overrides and regenerate: npm ci now passes on npm 10.8.2 linux/amd64
(SWC, rollup, and lightningcss platform binaries all install) and on
local npm 11, with verify:fast green on both.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 15:43:49 +01:00
jjbruntonandClaude Fable 5 90cde4aca4 fix(web): pin Next SWC binaries and regenerate lockfile with CI's npm
Since the SDK 56 dependency churn, arborist nests next under apps/web and
npm 10 skipped its platform-optional SWC binaries on the x64 CI runner,
breaking next build. Pin the linux and darwin binaries as direct
optionalDependencies of @sauci/web (bump alongside next) and regenerate
the lockfile from scratch with npm 10.8.2 to match CI. Verified in a
linux/amd64 node:20.19.4 container: npm ci installs the binary.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 15:29:01 +01:00
8 changed files with 26405 additions and 26391 deletions

No files matched your search

+8
View File
@@ -39,3 +39,11 @@ terraform
.claude .claude
.codex .codex
.ralph .ralph
# The admin app builds from this same monorepo context (apps/admin/Dockerfile).
!apps/admin
!apps/admin/**
# Host build artefacts must never leak into any image context.
**/node_modules
**/dist
+8 -3
View File
@@ -24,9 +24,14 @@ jobs:
echo "master is frozen; target staging or main" >&2 echo "master is frozen; target staging or main" >&2
exit 1 exit 1
fi fi
if [ "$BASE_BRANCH" = "main" ] && [ "$HEAD_BRANCH" != "staging" ]; then if [ "$BASE_BRANCH" = "main" ]; then
echo "Only staging may be promoted to main" >&2 case "$HEAD_BRANCH" in
exit 1 staging|hotfix/*|release/*) ;;
*)
echo "main takes staging promotions, or a hotfix/* or release/* branch for direct work" >&2
exit 1
;;
esac
fi fi
if [ "$BASE_BRANCH" != "staging" ] && [ "$BASE_BRANCH" != "main" ]; then if [ "$BASE_BRANCH" != "staging" ] && [ "$BASE_BRANCH" != "main" ]; then
echo "Feature pull requests target staging; promotions target main" >&2 echo "Feature pull requests target staging; promotions target main" >&2
+27 -22
View File
@@ -1,32 +1,37 @@
{ {
"name": "@sauci/web", "name": "@sauci/web",
"version": "1.0.0", "version": "1.0.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev",
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"lint": "eslint .", "lint": "eslint .",
"test": "vitest run --passWithNoTests", "test": "vitest run --passWithNoTests",
"typecheck": "tsc --noEmit" "typecheck": "tsc --noEmit"
}, },
"dependencies": { "dependencies": {
"framer-motion": "^11.0.0", "framer-motion": "^11.0.0",
"lucide-react": "^0.460.0", "lucide-react": "^0.460.0",
"next": "15.5.24", "next": "15.5.24",
"posthog-js": "^1.311.0", "posthog-js": "^1.311.0",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1" "react-dom": "^18.3.1"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20.0.0", "@types/node": "^20.0.0",
"@types/react": "~19.1.10", "@types/react": "~19.1.10",
"@types/react-dom": "~19.1.7", "@types/react-dom": "~19.1.7",
"autoprefixer": "^10.4.0", "autoprefixer": "^10.4.0",
"eslint": "^8.57.1", "eslint": "^8.57.1",
"eslint-config-next": "15.5.24", "eslint-config-next": "15.5.24",
"postcss": "^8.4.0", "postcss": "^8.4.0",
"tailwindcss": "^3.4.0", "tailwindcss": "^3.4.0",
"typescript": "^5.3.0" "typescript": "^5.3.0"
} },
} "optionalDependencies": {
"@next/swc-linux-x64-gnu": "15.5.24",
"@next/swc-linux-x64-musl": "15.5.24",
"@next/swc-darwin-arm64": "15.5.24"
}
}
+8 -4
View File
@@ -14,10 +14,14 @@ not deploy the retired Supabase data plane.
| `master` | frozen legacy default | none | none | | `master` | frozen legacy default | none | none |
Normal feature pull requests target `staging`. After CI and authenticated staging Normal feature pull requests target `staging`. After CI and authenticated staging
acceptance pass, open a promotion pull request from `staging` to `main`. Do not acceptance pass, open a promotion pull request from `staging` to `main`. Prefer
add production-only commits to `main`: production should run the exact commit that route: production then runs the exact commit already exercised in staging.
already exercised in staging. Direct pushes to either deployment branch are for
explicit incident recovery only. When production needs a change that cannot wait for the next promotion, open a
pull request into `main` from a `hotfix/*` or `release/*` branch. CI enforces
that naming so a direct-to-main change is always deliberate and visible. Land
the same commit on `staging` too, or the next promotion will revert it. Direct
pushes to either deployment branch remain blocked by branch protection.
Each Dokploy Compose application is connected to `jjbrunton/Sauci`, watches its Each Dokploy Compose application is connected to `jjbrunton/Sauci`, watches its
single branch with push-triggered automatic deployment, and uses single branch with push-triggered automatic deployment, and uses
+6 -3
View File
@@ -87,9 +87,12 @@ Before the first deployment, provide:
5. A rollback and restore point before any migrated production data is imported. 5. A rollback and restore point before any migrated production data is imported.
6. A separately generated `MEDIA_SIGNING_SECRET` (at least 32 characters) and 6. A separately generated `MEDIA_SIGNING_SECRET` (at least 32 characters) and
the HTTPS API origin as `MEDIA_PUBLIC_BASE_URL`. the HTTPS API origin as `MEDIA_PUBLIC_BASE_URL`.
7. The public web origin is fixed to `https://sauci.app` by 7. Browser origins are fixed by `CORS_ALLOWED_ORIGINS` in the Compose contract:
`CORS_ALLOWED_ORIGINS` in the Compose contract. Do not broaden it without a `https://sauci.app` (public web) and `https://manage.sauci.app` (admin
reviewed browser client requirement. console, which calls `/v1/admin/*` with a bearer token). Do not broaden the
list without a reviewed browser client requirement, and add any new admin
host here rather than as a Dokploy override, so the allowlist stays
reviewable in git.
8. A non-production hosted Auth project and disposable, onboarding-complete test 8. A non-production hosted Auth project and disposable, onboarding-complete test
identities mapped into the staging database for authenticated mobile acceptance. identities mapped into the staging database for authenticated mobile acceptance.
+2 -1
View File
@@ -8,7 +8,8 @@ x-common-environment: &common-environment
x-api-environment: &api-environment x-api-environment: &api-environment
<<: *common-environment <<: *common-environment
PORT: 3003 PORT: 3003
CORS_ALLOWED_ORIGINS: https://sauci.app # https://manage.sauci.app is the admin console; it calls /v1/admin/* from the browser.
CORS_ALLOWED_ORIGINS: https://sauci.app,https://manage.sauci.app
DATABASE_POOL_MAX: ${API_DATABASE_POOL_MAX:-10} DATABASE_POOL_MAX: ${API_DATABASE_POOL_MAX:-10}
DATABASE_POOL_IDLE_TIMEOUT_MS: ${DATABASE_POOL_IDLE_TIMEOUT_MS:-30000} DATABASE_POOL_IDLE_TIMEOUT_MS: ${DATABASE_POOL_IDLE_TIMEOUT_MS:-30000}
DATABASE_POOL_CONNECTION_TIMEOUT_MS: ${DATABASE_POOL_CONNECTION_TIMEOUT_MS:-10000} DATABASE_POOL_CONNECTION_TIMEOUT_MS: ${DATABASE_POOL_CONNECTION_TIMEOUT_MS:-10000}
+26276 -26289
View File
File diff suppressed because it is too large. Load diff
+70 -69
View File
@@ -1,69 +1,70 @@
{ {
"name": "sauci", "name": "sauci",
"private": true, "private": true,
"workspaces": [ "workspaces": [
"apps/api", "apps/api",
"apps/admin", "apps/admin",
"apps/mcp", "apps/mcp",
"apps/mobile", "apps/mobile",
"apps/supabase", "apps/supabase",
"apps/web", "apps/web",
"e2e", "e2e",
"packages/*" "packages/*"
], ],
"scripts": { "scripts": {
"postinstall": "node scripts/fix-hoisting.js", "postinstall": "node scripts/fix-hoisting.js",
"dev": "scripts/dev-local.sh up", "dev": "scripts/dev-local.sh up",
"dev:turbo": "turbo dev", "dev:turbo": "turbo dev",
"build": "turbo build", "build": "turbo build",
"lint": "turbo lint && npm run lint:harness", "lint": "turbo lint && npm run lint:harness",
"lint:harness": "node scripts/lint-harness.mjs", "lint:harness": "node scripts/lint-harness.mjs",
"test": "turbo test --filter=!@sauci/e2e", "test": "turbo test --filter=!@sauci/e2e",
"test:coverage": "npm run test:coverage -w @sauci/api && npm run test:coverage -w @sauci/mobile", "test:coverage": "npm run test:coverage -w @sauci/api && npm run test:coverage -w @sauci/mobile",
"test:supabase": "scripts/test-supabase.sh", "test:supabase": "scripts/test-supabase.sh",
"test:api:integration": "scripts/test-api-integration.sh", "test:api:integration": "scripts/test-api-integration.sh",
"test:mobile:e2e": "maestro test e2e/maestro", "test:mobile:e2e": "maestro test e2e/maestro",
"verify:fast": "npm run lint && npm run typecheck && npm test", "verify:fast": "npm run lint && npm run typecheck && npm test",
"verify:full": "npm run verify:fast && npm run build && npm run test:api:integration", "verify:full": "npm run verify:fast && npm run build && npm run test:api:integration",
"verify:e2e": "scripts/dev-local.sh e2e", "verify:e2e": "scripts/dev-local.sh e2e",
"verify": "npm run verify:full && npm run verify:e2e", "verify": "npm run verify:full && npm run verify:e2e",
"db:start": "npx supabase start --workdir apps", "db:start": "npx supabase start --workdir apps",
"db:stop": "npx supabase stop --workdir apps", "db:stop": "npx supabase stop --workdir apps",
"db:reset": "npx supabase db reset --workdir apps", "db:reset": "npx supabase db reset --workdir apps",
"db:migration:new": "npx supabase migration new --workdir apps", "db:migration:new": "npx supabase migration new --workdir apps",
"learn:check": "node scripts/validate-learnings.mjs", "learn:check": "node scripts/validate-learnings.mjs",
"clean": "turbo clean", "clean": "turbo clean",
"typecheck": "turbo typecheck" "typecheck": "turbo typecheck"
}, },
"dependencies": { "dependencies": {
"expo-router": "~56.2.20", "expo-router": "~56.2.20",
"react": "19.2.3", "react": "19.2.3",
"react-dom": "19.2.3", "react-dom": "19.2.3",
"react-native-safe-area-context": "~5.7.0", "react-native-safe-area-context": "~5.7.0",
"react-native-screens": "~4.26.0" "react-native-screens": "~4.26.0"
}, },
"devDependencies": { "devDependencies": {
"@types/react": "~19.1.10", "@types/react": "~19.1.10",
"@types/react-dom": "~19.1.7", "@types/react-dom": "~19.1.7",
"patch-package": "^8.0.1", "patch-package": "^8.0.1",
"react-test-renderer": "19.2.3", "react-test-renderer": "19.2.3",
"supabase": "^2.39.2", "supabase": "^2.39.2",
"pg": "^8.17.2", "pg": "^8.17.2",
"turbo": "^2.3.3", "turbo": "^2.3.3",
"typescript": "^5.3.3", "typescript": "^5.3.3",
"vitest": "^2.1.9" "vitest": "^2.1.9"
}, },
"packageManager": "npm@10.2.0", "packageManager": "npm@10.2.0",
"engines": { "engines": {
"node": ">=20 <21" "node": ">=20 <21"
}, },
"overrides": { "overrides": {
"rollup": "4.54.0", "rollup": "4.54.0",
"react-native": "0.85.3", "react-native": "0.85.3",
"expo": "~56.0.0" "expo": "~56.0.0",
}, "@react-native/metro-config": "0.85.3"
"optionalDependencies": { },
"@rollup/rollup-linux-arm64-musl": "4.54.0", "optionalDependencies": {
"@rollup/rollup-linux-x64-musl": "4.54.0" "@rollup/rollup-linux-arm64-musl": "4.54.0",
} "@rollup/rollup-linux-x64-musl": "4.54.0"
} }
}