Attendance worker
Cloudflare Worker API for attendance — Better Auth on D1 served through an Elysia app using the experimental Cloudflare Worker adapter. Transactional email is rendered with React Email and delivered via Resend (console in local dev).
Stack
- Framework: Elysia (
elysia/adapter/cloudflare-worker) — mountsauth.handlerat/api/auth, adds anauth: trueroute macro that injects typeduser/session. - Auth: Better Auth
1.6.26— email/password, GitHub & Google social sign-in, plususername,magicLink,twoFactorandorganizationplugins. - Organizations: Better Auth
organizationplugin 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/renderworkerd build) and delivered over plainfetchto the Resend REST API. In local development (noRESEND_API_KEY), envelopes are printed to the worker console instead. - Database: Drizzle ORM on the D1
DBbinding. Better Auth uses its generated schema insrc/db/auth-schema.ts; attendance members, cameras, event history, and daily counters live insrc/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
vp install
cp .dev.vars.example .dev.vars # fill in BETTER_AUTH_SECRET etc.
vp run dev # http://localhost:8787
Quick smoke test:
curl -X POST http://localhost:8787/api/auth/sign-up/email \
-H "Content-Type: application/json" \
-d '{"email":"[email protected]","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):
- Builds
{WEB_APP_URL}/invitations/{invitationId}. - Renders
emails/OrganizationInvitationEmail.tsxto HTML + plain text. - 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.
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:
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:
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 protected]> |
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.tsbuildssocialProvidersconditionally, so providers are only registered when their secrets are set.
Deploy
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