Skip to content

SPRINT-7 T5-B Phase 2 — api.last1.app public exposure

Date: 2026-08-30 (23:20 UTC)
Status: PHASE 2 COMPLETE — STOP for Ryan review
Authorization: Ryan T5-B Phase 2 go (Phase 1 SAFE TO EXPOSE confirmed)
Prior: SPRINT7_T5-B_PHASE1_RERUN_REPORT.md


Verdict

api.last1.app LIVE — public smoke ALL PASS

Production Supabase API hostname is exposed through Cloudflare proxied orange cloud → last1app-edge tunnel → Kong on VM 103 (192.168.1.103:8000).

STOP. Ryan reviews public RLS proof below before T5-C/T5-D (iOS pointer cutover, webhook URL rewrites, hosted teardown).


1. DNS + tunnel

Item Value
Hostname api.last1.app
Record CNAME → 6b0fa8e8-2eef-4550-9811-12e6ad31d10b.cfargotunnel.com
Proxied Yes (orange cloud)
Public A 104.21.32.216, 172.67.136.24 (Cloudflare anycast)
Tunnel VM last1app-app @ 192.168.1.104 (VM 121)
Tunnel ingress api.last1.app → http://192.168.1.103:8000
Script scripts/last1app/t5-b-dns-api.sh
Rollback backup /tmp/last1app-gate-b/t5b-api-dns-backup-20260830T231851Z

TLS terminates at Cloudflare (valid cert for api.last1.app, issuer Google Trust Services / Cloudflare edge).


2. Public smoke (through CF proxy — not LAN)

All requests showed cf-ray + server: cloudflare.

Test Result
GET /auth/v1/health + apikey HTTP 200
Anon GET /rest/v1/users?select=id&limit=1 0 rows, HTTP 200
Anon GET /rest/v1/check_ins?select=id&limit=1 0 rows, HTTP 200
Anon GET /rest/v1/units?select=id&limit=1 0 rows, HTTP 200
Edge anon POST sync-user-from-last1 401 (rejected)
Edge anon POST delete-account 401 (rejected)
Edge anon POST quick-check-in 400 (rejected, not 200)
Edge anon POST stripe-checkout 400 (rejected, not 200)
Edge cron anon POST sms-cron 401
Edge cron anon POST org-sms-cron 401
Storage profile-photos public path Reachable via CF (HTTP 400 without object path — bucket public)
Storage unit-chat-images anon list 0 objects, HTTP 200
DNS resolves to CF, not 192.168.* PASS

Final proof (Ryan-requested): RLS identity denial holds on the public path, not just LAN Kong.

Report: /tmp/last1app-gate-b/t5b-public-smoke-20260830T231956Z.txt
Script: scripts/last1app/t5-b-public-smoke.sh


3. Direct-origin bypass

Probe Result
Public DNS api.last1.app A records Cloudflare anycast only (no RFC1918)
Kong origin 192.168.1.103:8000 — LAN/Tailscale only, not routed on public internet
Traffic path Internet → CF WAF/proxy → tunnel → Kong

No public bypass to Kong without going through Cloudflare tunnel hostname.


4. iOS / RadioCheck archive pointer (host + anon key pairing)

Sovereign stack (VM 103) — use for tonight's archive build:

Env var Value
EXPO_PUBLIC_SUPABASE_URL https://api.last1.app
EXPO_PUBLIC_SUPABASE_ANON_KEY BWS LAST1APP_DB_PROD_SUPABASE_ANON_KEY (last1-radiocheck project)

JWT payload (sovereign anon key): role=anon, iss=supabase (stack-issued HS256 — not hosted ref tcthzakppndkykatoefx).

Current audit clone defaults (lib/supabase.ts): still fall back to hosted https://tcthzakppndkykatoefx.supabase.co + hosted anon JWT if env vars unset. Archive build must inject env (EAS secrets / .env.production) — code change not required if env is set at build time.

Auth note: Phone OTP / Apple SIWA on sovereign GoTrue are live (T5-A). Last1 auth client in supabase.ts remains pinned to hosted defaults for OTP; full cutover may require separate Last1 auth URL work (T5-C scope).

Staging reference (unchanged): https://last1app-db.last1.enterprises still works for internal smoke; production iOS should use api.last1.app.


5. Not in scope (deferred)

Item When
RadioCheck iOS TestFlight with new URL T5-C (Ryan + archive)
Twilio/Stripe webhook URL → api.last1.app T5-C/D
pg_cron URL rewrite 192.168.1.103 → https://api.last1.app Before activating SMS crons publicly
API_EXTERNAL_URL on VM 103 (still last1app-db.last1.enterprises) OAuth redirect URI alignment — separate pass
Hosted Supabase teardown After iOS + webhooks cut over

STOP

Phase 2 complete. Review public anon 0-row proof above before authorizing T5-C/T5-D.