Permit claiming web app for Ubiquity Rewards.
- Frontend: React + TypeScript + Vite in
src/. - Server:
serve.ts(Deno Deploy) serves the built SPA fromdist/and exposes a small API to record permit claims in Supabase.
- Install dependencies:
bun install
- Create a
.envfile:cp .env.example .env
- Fill in required variables (see
.env.example):- Backend (
serve.ts):SUPABASE_URL,SUPABASE_SERVICE_ROLE_KEY - Frontend (Vite):
VITE_SUPABASE_URL,VITE_SUPABASE_ANON_KEY - CI build convenience:
SUPABASE_ANON_KEYis used to setVITE_SUPABASE_ANON_KEYin.github/workflows/deno-deploy.yml
- Backend (
All frontend variables must be prefixed with VITE_ to be exposed to the app.
RPC uses /rpc only on *.ubq.fi hostnames; all other hostnames use https://rpc.ubq.fi.
-
Two‑port dev with HMR:
bun run dev
- Vite runs on
http://localhost:5173 serve.tsruns onhttp://localhost:8000- Vite proxies
/api/*and top‑level numeric routes to:8000.
- Vite runs on
-
Production‑like single‑port (no HMR):
bun run start
Builds the frontend and serves it + API on
http://localhost:8000.
bun run buildOutputs static files to dist/.
Deployment is handled by GitHub Actions via .github/workflows/deno-deploy.yml.
- Builds
dist/with Bun/Vite. - Deploys
serve.tsto Deno Deploy and includesdist/**. - Required Deploy secrets:
DENO_DEPLOY_TOKEN,SUPABASE_URL,SUPABASE_ANON_KEY,SUPABASE_SERVICE_ROLE_KEY.
serve.ts exposes POST /api/permits/record-claim which accepts { "transactionHash": "0x...", "networkId": 100 } and derives the permit signature(s) by decoding Permit2 calldata before updating Supabase.
Note: this endpoint uses SUPABASE_SERVICE_ROLE_KEY (bypasses RLS), so security comes from on-chain transaction verification before writing.
One-off tooling lives in scripts/ (requires .env with Supabase + RPC vars).
- Audit permits against both Permit2 contracts (includes “nonce used but DB transaction missing” mismatches):
bun run permit2:audit -- --owner 0x... --include-permits --out /tmp/permit2-audit.json
- Build an invalidation plan (no transactions sent):
bun run permit2:invalidate -- --owner 0x... --target old --only-db-unclaimed --out /tmp/permit2-invalidate-plan.json
- Execute invalidations (sends transactions; use
--max-txsas a safety brake):INVALIDATOR_PRIVATE_KEY=0x... bun run permit2:invalidate -- --owner 0x... --target old --only-db-unclaimed --execute --max-txs 50 --out /tmp/permit2-invalidate-executed.json
- Backfill missing
permits.transactionvalues for claim txs:bun run permit2:backfill -- --owner 0x... --out /tmp/permit2-backfill.json- For a more complete one-off backfill on Gnosis, add
--match-mode beneficiary --scan-new-permit2-txlist. - Add
--executeto write to Supabase (use--max-updatesas a safety brake).
- Seed small test permits for QA (default: 1e18 UUSD on Gnosis; plan-only unless
--execute):INVALIDATOR_PRIVATE_KEY=0x... bun run permit2:seed-test-permits -- --beneficiary 0x... --beneficiary-user-id <githubUserId> --execute --out /tmp/test-permits.json
- Sign/broadcast test permit claims (sign-only unless
--execute):BENEFICIARY_PRIVATE_KEY=0x... bun run permit2:claim-test-permits -- --beneficiary 0x... --limit 10 --execute --batch --out /tmp/test-claims.json
bun run format / bun run format:check run Prettier across the repo (including serve.ts).
Permit2 and PermitAggregator addresses used by the UI are in src/constants/config.ts.
worker/entry.ts + wrangler.toml serve the Vite dist/ bundle from
Cloudflare Workers with the previous serve.ts routing (files, SPA fallback,
POST /api/permits/record-claim).
Deployed now: https://pay-ubq-fi.ubq.workers.dev (verified root, assets, SPA
route, and the claim endpoint's validation paths). The claim handler from
serve.ts is ported into the worker (RPC checks, Permit2 decode via viem,
Supabase REST read/update) and covered by worker/entry_test.ts
(bun test worker/entry_test.ts).
pay.ubq.fi is cut over to the worker (zone route pay.ubq.fi/*), so the UI,
assets, and SPA routing are live. SUPABASE_URL is set as a Worker secret; the
claim endpoint still answers {"error":"Supabase not configured"} until
SUPABASE_SERVICE_ROLE_KEY is supplied.
The anon key cannot substitute: a targeted no-op update against the unclaimed
row proved RLS filters anonymous UPDATEs (0 rows touched), so recording claims
genuinely requires the service-role key. Build with VITE_SUPABASE_URL and
VITE_SUPABASE_ANON_KEY set (org-level Supabase values).
