How the deployed pieces fit together, and the traps that have actually cost time here. No credentials appear in this file, and none should be added to it.
GitHub Actions (sync.yml)
scheduled every 5 min
│
│ POST /api/sync
│ Authorization: CRON_SECRET
▼
browser ──────▶ Vercel project `web` ──────▶ Supabase Postgres
(Next.js, apps/web) pooler payments, sync_state
│
│ Soroban RPC (read-only)
▼
Stellar testnet
ReceiptAnchor · RefundVault
Three moving parts, and only one of them is Vercel's:
web— the Next.js app inapps/web. Serves the dashboard, the verifier, and the API routes. Aliased toaccensa-dashboard.vercel.app.- Supabase Postgres — holds
payments(indexed chain data) andsync_state(the indexer's ledger cursor). Schema is created on first request; seedb-setup.md. - GitHub Actions — the real indexing cadence. Explained below, because the
vercel.jsoncron is misleading on its own.
The documentation site in apps/docs is not deployed on Vercel. It is built
by GitHub Actions and published to GitHub Pages at
https://accensa.github.io/accensa-app/.
A second Vercel project named
docsused to serveaccensa-docs.vercel.appfrom the same directory. It was last deployed on 2026-07-13 and had drifted into serving unmodified Docusaurus scaffold copy, while every "Documentation" link in the org still pointed at it. The project was deleted on 2026-08-14;accensa-docs.vercel.appnow returns 404 and nothing should link to it.
apps/web/vercel.json declares a daily cron. That is not the real cadence,
and it is not a design choice:
On the Vercel Hobby plan, declaring more than one cron run per day is a hard deploy failure, not a silent clamp. The deploy is rejected.
So the schedule that matters lives in .github/workflows/sync.yml, which hits
/api/sync every 5 minutes. GitHub throttles high-frequency scheduled workflows,
so in practice it lands every one to three hours. apps/web/src/lib/sync-status.ts
sets its staleness thresholds from that observed behaviour rather than from the
declared schedule, so a normal gap is not reported to the merchant as a fault.
If you move off Hobby, raise the vercel.json cron and retire the Actions
workflow — do not run both, or the cursor gets contention from two writers.
The cadence is not cosmetic. Soroban RPC serves getEvents for roughly the
last 121,000 ledgers, about a week of testnet. If the cursor stops advancing for
longer than that it falls outside the retained window, and the ledgers in between
are unrecoverable — no later run can reach them. A sync that skipped ledgers this
way reports skippedLedgers in its response and sync.yml raises a warning; it
is the one failure here that cannot be fixed by running the job again.
{
"success": true,
"latestLedger": 4067288,
"startLedger": 3967288,
"syncedTo": 4067288,
"skippedLedgers": 0,
"drained": true,
"pages": 11,
"windows": 11,
"scanned": 1,
"decoded": 1,
"inserted": 1
}| Field | Meaning |
|---|---|
syncedTo |
Where the cursor now stands. A reply without this field is not the indexer — sync.yml fails the run on it. |
drained |
False when paging stopped against the time budget. Not a fault; the next run resumes from syncedTo. |
windows |
getEvents calls made. Requests are bounded to 10,000 ledgers because the RPC silently truncates wider ranges. |
skippedLedgers |
Ledgers lost to the retention window. Should always be 0. |
scanned / decoded / inserted |
Events matched, decoded as transfers, and written. |
Use the Session pooler connection string, not Direct.
Direct is IPv6-only. Vercel Functions have no IPv6 route, so a Direct URL produces connection timeouts that look like a database outage and are not.
The pooler host looks like aws-1-<region>.pooler.supabase.com.
| Variable | Where | What it does |
|---|---|---|
DATABASE_URL |
Vercel (web) |
Supabase session pooler connection string. |
CRON_SECRET |
Vercel (web) + GitHub secret |
Shared by /api/sync and sync.yml. Anonymous callers get {"error":"Unauthorized"}. |
SYNC_URL |
GitHub secret | The /api/sync endpoint the workflow posts to. |
HOOK_API_KEY |
Vercel (web) |
Gates /api/hook/settle. Absent means the endpoint fails closed, not open. |
NEXT_PUBLIC_REFUND_VAULT_ID |
Vercel (web), optional |
Overrides the built-in RefundVault contract id. |
STELLAR_NETWORK_PASSPHRASE |
Vercel (web), optional |
Stellar network passphrase for auth challenges and RPC calls. Defaults to Test SDF Network ; September 2015. Set to Public Global Stellar Network ; September 2015 for pubnet. |
Set them per environment (production, preview, development) — Vercel does
not share values across them.
Agent and CI shells default to --non-interactive, and piping to stdin in that
mode stores nothing without erroring:
vercel env add DATABASE_URL production --value "$CONNECTION_STRING" # correct
echo "$CONNECTION_STRING" | vercel env add DATABASE_URL production # stores emptyvercel env ls listing the key proves the key exists. It proves nothing about
its contents.
Vercel also marks new variables sensitive by default, and vercel env pull
returns sensitive values blank by design — a blank in your local pull is not
evidence the remote value is blank. Use --no-sensitive for non-secrets if you
want to read them back.
Two steps, not one:
git checkout main && git pull
vercel deploy --prod
vercel alias set <new-deployment-url> accensa-dashboard.vercel.app--prod alone does not move the alias. accensa-dashboard.vercel.app is
assigned manually, so a production deploy leaves it pointing at the previous
build and the live site looks unchanged. This has caught people more than once.
Environment variables apply to new builds only. Changing one requires a redeploy before it takes effect.
The repo root .vercel/project.json is linked to project web; that link is
what makes monorepo deploys resolve correctly from the root.
curl -s -o /dev/null -w "%{http_code}\n" https://accensa-dashboard.vercel.app/
curl -s -o /dev/null -w "%{http_code}\n" https://accensa-dashboard.vercel.app/dashboard
curl -s -o /dev/null -w "%{http_code}\n" https://accensa-dashboard.vercel.app/verify
curl -s -o /dev/null -w "%{http_code}\n" https://accensa-dashboard.vercel.app/batches/1
curl -s -o /dev/null -w "%{http_code}\n" https://accensa-dashboard.vercel.app/batches/999 # expect 404
curl -s https://accensa-dashboard.vercel.app/api/sync # expect UnauthorizedThe /batches/999 404 and the /api/sync rejection are the two that catch a
half-configured deploy — a 200 from either means something is wrong.
Supabase → Settings → Database → Reset database password
Then update DATABASE_URL in every Vercel environment that uses it and
redeploy, since env changes only reach new builds. Take the session pooler
string, per the IPv6 note above.