All-in-one link-in-bio for creators. Built with React Router 7, Cloudflare Workers, Supabase, and Tailwind 4.
- Framework: React Router 7 (SSR, declarative routes)
- Runtime: Cloudflare Workers
- Database: Supabase (Postgres + Auth + Realtime + Edge Functions)
- Styling: Tailwind CSS 4 + shadcn/ui (new-york)
- State: Zustand, React Hook Form, nuqs
- Payments: USDC on Base via x402
- Node.js ≥ 20
- pnpm (
corepack enable) - Supabase CLI
- Wrangler (installed as devDep)
# Install dependencies
pnpm install
# Copy local secrets template
cp .dev.vars.example .dev.vars
# Edit .dev.vars — Supabase URL, anon key, **service role key** (Worker purchase checks),
# plus Privy/Worker secrets referenced in this file.
# Optionally adjust `wrangler.jsonc` `vars` (placeholders checked in).
# Prefer Cloudflare Dashboard **secrets** for keys that must not ship in git (`wrangler secret put …`).
# Generate Cloudflare types
pnpm cf-typegen
# Start dev server
pnpm devMerges to main run .github/workflows/deploy.yml. Treat GitHub Actions as the canonical production deploy path.
Important: .dev.vars exists only on your machine (gitignored). CI never reads it. The workflow runs scripts/deploy-worker-ci.sh, which passes wrangler deploy --var … so plaintext bindings (SUPABASE_URL, SUPABASE_ANON_KEY, PRIVY_APP_ID, PRIVY_SERVER_SIGNER_ID, etc.) come from GitHub repository Secrets, not from wrangler.jsonc placeholders. Without those secrets, the deploy step fails loudly instead of pushing your-* placeholders that break Privy/login.
After pnpm build, Wrangler reads .wrangler/deploy/config.json and deploys from build/server/wrangler.json, which still contains the your-* placeholders mirrored from wrangler.jsonc. A deploy that skips deploy-worker-ci.sh (--var overrides from GitHub Secrets or .dev.vars) can overwrite live plaintext Worker vars with those placeholders again.
Local deploys are optional. Use pnpm run deploy / pnpm run deploy:apply-env only when your shell or .dev.vars includes the deploy credentials (CLOUDFLARE_API_TOKEN, CLOUDFLARE_ACCOUNT_ID) plus the plaintext Worker vars listed below. pnpm run deploy:raw is the intentionally unsafe escape hatch for pnpm run build && wrangler deploy (no --var), useful only for manual debugging when plaintext bindings are managed elsewhere. Cloudflare Workers Builds is connected to the repo but is not the canonical deploy path unless its build environment is given the same required vars/secrets; when those vars are absent, deploy-worker-ci.sh intentionally skips deployment under WORKERS_CI=1 so Workers Builds can stay green without overwriting GitHub Actions deployments.
| Repository secret | Required |
|---|---|
CLOUDFLARE_API_TOKEN, CLOUDFLARE_ACCOUNT_ID |
Yes |
SUPABASE_URL, SUPABASE_ANON_KEY, PRIVY_APP_ID, PRIVY_SERVER_SIGNER_ID |
Yes |
PUBLIC_SITE_HOST |
No — defaults to rare.xyz |
BASE_NETWORK |
No — defaults to base-sepolia |
WALLETCONNECT_PROJECT_ID |
No |
Worker Secrets (e.g. PRIVY_APP_SECRET, APP_JWT_SECRET, SUPABASE_SERVICE_ROLE_KEY, R2_UPLOAD_SECRET) are not listed in committed vars; configure them once in the Cloudflare Dashboard (or wrangler secret put) — they persist across deploys unless you remove them.
Wrangler never prints secret values (wrangler secret list shows names only).
- Plain-text
varsas deployed: runscripts/cloudflare-worker-bindings.shafterexport CLOUDFLARE_API_TOKEN=…andexport CLOUDFLARE_ACCOUNT_ID=…in the same shell (these are not read from.dev.vars). Use real values, not placeholders: paste the 32-character lowercase hex from"account_id"inwrangler.jsonc—not the literal stringyour-account-id. Create a token under API Tokens with Workers Scripts → Read on the account. Optionally setCLOUDFLARE_WORKER_SCRIPT_NAMEif it differs fromsuperlinks. Output is Cloudflare API JSON — plaintext vars may expose values, so paste carefully. - Runtime hints (no secrets): open
/loginor/signupon the deployed Worker, then Cloudflare Observability → Workers Logs → search[superlinks-env]. Loaders emit masked diagnostics when placeholders are detected.
Migrations live in supabase/migrations/. Push them with:
supabase db pushGenerate TypeScript types:
SUPABASE_PROJECT_ID=your-project-id pnpm db:types| Script | Description |
|---|---|
pnpm dev |
Start dev server |
pnpm build |
Production build |
pnpm run deploy |
Build + deploy-worker-ci.sh with --var (safe local path when .dev.vars includes deploy credentials) |
pnpm run deploy:apply-env |
Build + deploy-worker-ci.sh with --var (loads .dev.vars locally; GH Actions invokes the script separately after pnpm run build) |
pnpm run deploy:raw |
Build + raw wrangler deploy (escape hatch; can redeploy placeholder plaintext vars) |
pnpm cf-typegen |
Generate Cloudflare + RR types |
pnpm db:types |
Generate Supabase types |
pnpm typecheck |
Type check |
pnpm check |
Type check + build + deploy dry run |
pnpm knip |
Find unused exports/deps |
tests/test-app-hosting.sh |
Smoke checks for hosted ZIP apps (requires login + deployed worker for full run) |
scripts/cloudflare-worker-bindings.sh |
Curl Worker …/workers/scripts/$name/settings (plaintext vars visible; secrets omitted) |
Creators upload a ZIP static site from Dashboard → Products (App) or edge add-app.
- Served at
/app/:productSlug/on the Worker (paths with multiple segments are handled before React Router). - Legacy UUID deeplink
/app/:uuidredirects to slug when known, otherwise falls back to the commerce edge/HTML embed path.
Infrastructure
- Cloudflare R2 bucket (
rare-xyzby default — seewrangler.jsonc). - Wrangler secrets:
SUPABASE_SERVICE_ROLE_KEY(PostgREST purchase checks; never expose client-side);R2_UPLOAD_SECRET,APP_TOKEN_SECRET(must match commerce edge);APP_JWT_SECRETfor sessions. - Supabase
commercefunction secrets: sameR2_UPLOAD_SECRETandAPP_TOKEN_SECRET. WORKER_URL(recommended): HTTPS origin where this Worker is deployed (https://…workers.devor a custom Workers hostname). UploadsPUT /api/r2-uploadhit this URL. If unset, commerce useshttps://PUBLIC_SITE_HOSTinstead — that hostname must terminate on this Worker. A marketing site or SPA host that does not routePUT /api/r2-uploadto Workers will respond 404.
If this repo previously contained live Supabase keys in wrangler.jsonc, rotate anon and service-role keys in the Supabase dashboard after switching to placeholders.
Troubleshooting R2 upload failed … : 404
Edge add-app / update-app calls your Worker upload route. HTTP 404 means the URL ${WORKER_URL or site origin}/api/r2-upload is not handled by this worker (wrong host or domain not wired to Workers). Fix: set WORKER_URL on the commerce function to your Worker deployment origin, redeploy commerce, retry. Verify with something like curl -i -X PUT -H 'x-upload-secret: …' -H 'x-r2-key: test/ping' against that origin /api/r2-upload (expect 401 without secret match, 200 with correct secret—not 404 from the edge).
Login / signup stuck on “Loading…”
Committed wrangler.jsonc uses placeholder PRIVY_APP_ID, SUPABASE_*, etc. Every deploy that must carry real plaintext values needs deploy-worker-ci.sh / pnpm deploy:apply-env (CI secrets loaded as env vars, or .dev.vars locally). Editing plaintext vars only in the Cloudflare Dashboard is not enough if a later pnpm deploy (placeholder) runs. Without overrides, PrivyClientProvider never loads the SDK and the button stays idle. Also add https://YOUR-WORKER_SUBDOMAIN.workers.dev (and production domains) under Privy → Application → Domains / allowed origins so the iframe can initialize.
app/
├── routes/ # Thin route modules (loader/action/component)
├── features/ # Feature-first organization
│ ├── auth/ # Login, signup, callback, session helpers
│ ├── editor/ # Link page editor (My Links)
│ ├── store/ # Product CRUD, posts
│ ├── wallet/ # Earn, balance, transactions
│ ├── insights/ # Analytics
│ ├── messages/ # Chat with Supabase Realtime
│ ├── admin/ # Admin panel
│ ├── creator-page/ # Public /:handle page
│ ├── discover/ # Browse creators
│ └── app-viewer/ # Hosted app iframe
├── components/
│ ├── ui/ # shadcn/ui primitives
│ └── shared/ # Cross-feature components
├── lib/ # Shared infra (env, supabase, commerce, cache)
├── stores/ # Zustand stores
└── types/ # Generated types
workers/
├── app.ts # Cloudflare Workers entry (RR + R2 app gate)
└── app-hosting.ts # R2 ingest + `/app/:slug/` static hosting
supabase/
└── migrations/ # SQL migrations (source of truth)
| Path | Description |
|---|---|
/ |
Marketing landing page |
/login |
Google OAuth login |
/signup |
Claim username + sign up |
/auth/privy/exchange |
Exchanges a Privy access token for an HttpOnly session cookie |
/auth/refresh |
Refreshes the session cookie when the JWT is near expiry |
/auth/logout |
Clears the session cookie |
/docs |
CLI documentation |
/discover |
Browse creators |
/app/:id |
UUID deep link; redirects to /app/:slug/ when product has a slug (legacy embed otherwise) |
/app/:slug/... |
Hosted static app files from R2 (Worker, not React Router) |
/dashboard/links |
Editor: profile, links, theme |
/dashboard/products |
Product + post management |
/dashboard/insights |
Analytics |
/dashboard/earn |
Wallet + transactions |
/dashboard/messages |
Conversations |
/dashboard/admin |
Admin panel |
/dashboard/settings |
Account + theme toggle |
/:handle |
Public creator page (catch-all, must be last) |