A modern, opinionated starter template for building fast, accessible web applications.
- Astro v6 - Modern web framework with server-first rendering
- React v19 - UI library for interactive components
- TypeScript v5 - Type-safe JavaScript
- Tailwind CSS v4 - Utility-first CSS framework
- Supabase - Authentication and backend-as-a-service
- Cloudflare Workers - Edge deployment runtime
- Node.js v22.14.0 (as specified in
.nvmrc) - npm (comes with Node.js)
- Clone the repository:
git clone https://github.com/przeprogramowani/10x-astro-starter.git
cd 10x-astro-starter- Install dependencies:
npm install-
Set up Supabase and configure environment variables — see Supabase Configuration below.
-
Create a
.dev.varsfile for local Cloudflare dev secrets:
cp .env.example .dev.vars- Run the development server:
npm run devnpm run dev- Start development server (Cloudflare workerd runtime)npm run build- Build for productionnpm run preview- Preview production buildnpm run lint- Run ESLint with type-checked rulesnpm run lint:fix- Auto-fix ESLint issuesnpm run format- Run Prettiernpm run test:e2e- Run Playwright E2E testsnpm run test:e2e:ui- Run Playwright in interactive UI mode
Playwright drives the north-star smoke test (e2e/north-star.spec.ts). One-time setup:
- Start local Supabase:
npx supabase start. - Create the dedicated e2e user via
/auth/signup— emaile2e-north-star@local.test(see@.env.e2e.example). - Copy
.env.e2e.exampleto.env.e2eand fillE2E_USER_PASSWORD..env.e2eis gitignored. npm run test:e2e— Playwright boots (or reuses) the dev server, runs@playwright/setup/auth.setup.tsto capture a session intoplaywright/.auth/user.json, then executes the specs.
The dev server started by webServer sets OPENROUTER_MOCK=1 so tests never hit the real OpenRouter API.
.
├── src/
│ ├── layouts/ # Astro layouts
│ ├── pages/ # Astro pages
│ │ └── api/ # API endpoints
│ ├── components/ # UI components (Astro & React)
│ └── assets/ # Static assets
├── public/ # Public assets
├── wrangler.jsonc # Cloudflare Workers configThis project uses Supabase for authentication. Environment variables are declared via Astro's astro:env schema and are treated as server-only secrets — they are never exposed to the client.
Requires Docker and ~7 GB RAM.
- Create your
.envfile:
cp .env.example .env- Initialize the local Supabase project (creates a
supabase/config folder):
npx supabase init- Start the local stack (downloads Docker images on first run):
npx supabase start- Copy the credentials printed by the CLI into your
.envand.dev.vars:
SUPABASE_URL=http://127.0.0.1:54321
SUPABASE_KEY=<anon key from CLI output>
- To stop the stack when done:
npx supabase stopThe local Studio UI is available at http://localhost:54323.
Migrations live under supabase/migrations/. Apply them locally with npx supabase db reset (destroys local DB, replays every migration from zero, seeds if supabase/seed.sql exists). Run pgTAP tests with npx supabase test db.
If you prefer to use a hosted Supabase project, add these variables to your .env and .dev.vars files:
| Variable | Description |
|---|---|
SUPABASE_URL |
Project URL from Supabase dashboard → Settings → API |
SUPABASE_KEY |
anon public key from Supabase dashboard → Settings → API |
SUPABASE_URL=https://<project-ref>.supabase.co
SUPABASE_KEY=<anon-key>
TypeScript types for the schema live at src/db/database.types.ts and are checked into git. Regenerate them after any migration change:
npx supabase db reset # apply latest migrations to the local stack
npm run db:types # dump typescript definitions into src/db/database.types.tsCommit the regenerated file with the migration that produced it. CI does not run Supabase, so a stale database.types.ts will surface as an astro sync / build failure on push.
By default Supabase requires email confirmation before a user can sign in. To skip this during local development:
- Open the Supabase dashboard for your project
- Go to Authentication → Email → Confirm email
- Toggle it off
Users can then sign in immediately after sign-up without clicking a confirmation link.
| Route | Description |
|---|---|
/auth/signin |
Email/password sign-in form |
/auth/signup |
Email/password sign-up form |
/auth/confirm-email |
Post-signup "check your inbox" page |
/dashboard |
Example protected page (redirects to /auth/signin if unauthenticated) |
Route protection is handled in src/middleware.ts. Add paths to the PROTECTED_ROUTES array there to require authentication.
Users can delete their account from /account. The flow is soft-delete + retention window rather than immediate hard-delete:
- User clicks Usuń konto in
/account→AlertDialogrequires them to type their exact email. POST /api/account/deletecallsenqueue_hard_delete(user_id)which setsprofiles.deleted_at = now()andscheduled_hard_delete_at = now() + 30d, thensupabase.auth.signOut({ scope: "global" })revokes every refresh token for the user.- Any subsequent request with an old cookie hits the middleware soft-delete gate and is redirected to
/auth/restore-account. RLSEXISTSgates oncardsandreview_historyalso block reads/writes even if a token slips through. - If the user logs in again within the 30-day window they land on
/auth/restore-accountand can click Przywróć konto →POST /api/account/restorecallsrestore_account()which clearsdeleted_at. Data (fiszki, historia FSRS) is byte-identical. - Two
pg_cronjobs run daily on Supabase:hard_delete_expired_accounts@ 03:00 UTC — deletes fromauth.usersfor every row past cutoff. FK CASCADE clearscards,review_history,profiles.retention_watchdog@ 04:00 UTC — RAISE EXCEPTION when any profile is more than 1 day past its cutoff. The failed job appears red in Supabase Studio → Cron Jobs → History (fail-loud, no external alerting infra required).
Sign-up on an email in the retention window is blocked. POST /api/auth/signup pre-checks via the email_pending_deletion RPC and redirects with ?error=account_pending_deletion + a link to sign in and restore.
- Supabase Studio → Database → Cron Jobs → History:
- Green rows for
hard_delete_expired_accounts(03:00 UTC daily) — normal. - Red row for
retention_watchdog(04:00 UTC daily) = orphans past cutoff. Investigate:Then checkselect * from public.profiles where scheduled_hard_delete_at is not null and scheduled_hard_delete_at < now() - interval '1 day';
cron.job_run_detailsforhard_delete_expired_accountsfailures the previous day.
- Green rows for
- Ad-hoc query (manual double-check outside Studio):
select count(*) from public.profiles where scheduled_hard_delete_at is not null and scheduled_hard_delete_at < now() - interval '1 day'; -- 0 = healthy; >0 = investigate as above.
retention_watchdog is fail-loud (RAISE EXCEPTION), so a red job in Studio is the signal, not just an elevated counter.
This project deploys to Cloudflare Workers.
- Build the project:
npm run build- Deploy with Wrangler:
npx wrangler deploySet SUPABASE_URL and SUPABASE_KEY as secrets in your Cloudflare dashboard or via npx wrangler secret put.
GitHub Actions runs lint + build on every push and PR to master. Configure SUPABASE_URL and SUPABASE_KEY as repository secrets in GitHub for the build step.
MIT
