Files
Attendance/apps/worker
..

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) — 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

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):

  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.

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.ts builds socialProviders conditionally, 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