# Attendance worker Cloudflare Worker API for **attendance** — [Better Auth](https://better-auth.com) on [D1](https://developers.cloudflare.com/d1/) served through an [Elysia](https://elysiajs.com) app using the experimental Cloudflare Worker adapter. Transactional email is rendered with [React Email](https://react.email) and delivered via [Resend](https://resend.com) (console in local dev). ## Stack - **Framework**: [Elysia](https://elysiajs.com/integrations/cloudflare-worker.html) (`elysia/adapter/cloudflare-worker`) — mounts `auth.handler` at `/api/auth`, adds an `auth: true` route macro that injects typed `user`/`session`. - **Auth**: Better Auth `1.6.26` — email/password, GitHub & Google social sign-in, plus `username`, `magicLink`, `twoFactor` and `organization` plugins. - **Organizations**: Better Auth `organization` plugin with default owner/admin/member roles and native teams. Organizations may contain up to 25 teams and 100 accounts per team; dynamic roles remain disabled. Any authenticated user may create up to 10 organizations; 100 members and 100 pending invitations per organization; invitations expire after 48h; re-inviting an address cancels its previous pending invitation; deletion is owner-only and enabled. - **Email**: React Email templates rendered on the Worker (`@react-email/render` workerd build) and delivered over plain `fetch` to the Resend REST API. In local development (no `RESEND_API_KEY`), envelopes are printed to the worker console instead. - **Database**: Drizzle ORM on the D1 `DB` binding. Better Auth uses its generated schema in `src/db/auth-schema.ts`; attendance members, cameras, event history, and daily counters live in `src/db/attendance-schema.ts`. ## Project layout ``` src/ index.ts # Elysia app (entrypoint) env.d.ts # Type augmentation for secret bindings db/ index.ts # Typed Drizzle client for env.DB schema.ts # Barrel exported to the runtime Drizzle client/Kit auth-schema.ts # Better Auth + organization schema (CLI-generated) attendance-schema.ts # Members, cameras, attendance events + daily summaries lib/ auth/ auth.ts # Runtime auth instance (Drizzle adapter) auth-options.ts # Shared auth config (plugins, providers, CORS origins) auth.generate.ts # CLI-only Drizzle config for `auth generate` invitation-email.ts# organization.sendInvitationEmail callback (URL + delivery) magic-link-email.ts# magicLink.sendMagicLink callback email/ render-email.ts # React Email → { html, text } send-email.ts # Transport selection (Resend vs console) types.ts # EmailEnvelope / EmailTransport / EmailDeliveryError transports/ resend.ts # Resend REST API over fetch (idempotency-key aware) console.ts # Local-dev-only console transport emails/ components/EmailLayout.tsx OrganizationInvitationEmail.tsx MagicLinkEmail.tsx drizzle/ 0000_*.sql # Better Auth + organization migration 0001_*.sql # Attendance domain migration 0002_*.sql # Organization teams + account profile fields meta/ # Drizzle schema snapshots and journal drizzle.config.ts # Drizzle Kit schema/output configuration wrangler.jsonc # D1 binding, vars, compat flags ``` ## Local development ```bash vp install cp .dev.vars.example .dev.vars # fill in BETTER_AUTH_SECRET etc. vp run dev # http://localhost:8787 ``` Quick smoke test: ```bash curl -X POST http://localhost:8787/api/auth/sign-up/email \ -H "Content-Type: application/json" \ -d '{"email":"a@b.c","password":"password123","name":"A"}' ``` Organization endpoints require an `Origin` header, e.g. `-H "Origin: http://localhost:5173"`. ## Invitation emails Better Auth does **not** send invitation emails or generate invitation URLs — the app does. `organization.sendInvitationEmail` (in `lib/auth/invitation-email.ts`): 1. Builds `{WEB_APP_URL}/invitations/{invitationId}`. 2. Renders `emails/OrganizationInvitationEmail.tsx` to HTML + plain text. 3. Delivers via `sendEmail` (Resend in production, console in local dev). The web app route `/invitations/:invitationId` loads the invitation by ID (Better Auth validates the session email), lets the user accept/decline, then explicitly activates the joined organization. Magic links reuse the same render + transport pipeline. Invitation IDs and auth links are never logged outside local development; the console transport exists only for `localhost`/`127.0.0.1` runs. ## Migrations The barrel in `src/db/schema.ts` exports the generated auth schema and the hand-maintained attendance schema. Drizzle Kit generates committed SQL in `drizzle/`, and Wrangler applies it to D1 outside the request lifecycle. ```bash vp run db:generate # generate SQL after a schema change vp run db:check # validate migration history vp run db:migrate:local # apply to local dev DB (.wrangler/state) vp run db:migrate:remote # apply to the configured remote D1 database ``` When Better Auth options or plugins change, regenerate only `auth-schema.ts`, review it, then generate a migration. Attendance tables stay isolated so the CLI cannot overwrite them: ```bash vp run auth:schema vp run db:generate vp run db:migrate:local ``` `auth.generate.ts` exists because the CLI runs in Node without the Worker's D1 binding. It shares `auth-options.ts` with runtime auth and supplies an inert typed D1 client; schema generation does not execute database queries. For a new remote environment, create the D1 database and replace the placeholder `database_id` in `wrangler.jsonc` before running the remote migration: ```bash wrangler d1 create attendance-db ``` ## Secrets & env | Binding | Type | Notes | | ------------------------------------------- | ----------- | --------------------------------------------------------------------------------------------------------------------------------------- | | `DB` | D1 | `attendance-db` | | `BETTER_AUTH_URL` | var | Base URL, `http://localhost:8787` in dev | | `TRUSTED_ORIGINS` | var | Comma-separated extra CORS origins (e.g. `http://localhost:5173`) | | `WEB_APP_URL` | var | Web app base URL used to build invitation/magic-link URLs (e.g. `http://localhost:5173`) | | `EMAIL_FROM` | var | Sender for transactional email, e.g. `Attendance ` | | `EMAIL_REPLY_TO` | var | Optional reply-to for transactional email | | `BETTER_AUTH_SECRET` | **secret** | `wrangler secret put BETTER_AUTH_SECRET`; ≥32 chars | | `RESEND_API_KEY` | **secret** | `wrangler secret put RESEND_API_KEY`; enables production email delivery. Without it, email is only printed to the console in local dev. | | `GITHUB_CLIENT_ID` / `GITHUB_CLIENT_SECRET` | **secrets** | optional — enables GitHub sign-in | | `GOOGLE_CLIENT_ID` / `GOOGLE_CLIENT_SECRET` | **secrets** | optional — enables Google sign-in | > `src/lib/auth/auth-options.ts` builds `socialProviders` conditionally, so providers are only registered when their secrets are set. ## Deploy ```bash vp run cf-typegen # after changing wrangler.jsonc bindings wrangler secret put BETTER_AUTH_SECRET wrangler secret put RESEND_API_KEY # required for production email vp run db:migrate:remote vp run deploy ```