SacTech website #
The community website for SacTech. It is a Next.js application deployed on Netlify, backed by Netlify Database and Drizzle ORM, with Better Auth providing email/password accounts and admin roles.
The event flow is deliberately moderated:
- A person creates an account or signs in at
/auth. - From
/account, they open/events/submitand send an event for review. New events always start aspending, and the account page tracks their status. - A SacTech admin reviews the queue at
/admin/eventsand approves or rejects each event. - Only
approvedevents are returned to the public/eventscalendar.
Authorization and status checks are enforced on the server. Hiding an admin link or changing a form value in the browser is not enough to bypass them.
Stack #
- Next.js 16 App Router and React 19
- Netlify's Next.js runtime, configured by
netlify.toml - Netlify Database, a managed Postgres database
- Drizzle ORM's native Netlify Database driver
- Better Auth with its Drizzle adapter, email/password authentication, and Admin plugin
Netlify Database is currently available only on Netlify's Credit-based plans. Database compute and bandwidth consume credits; review the current billing and limits documentation before enabling it for the project.
Local development #
Prerequisites #
- Node.js 24 (
.nvmrcpins the project to Node.js 24.19.0) - Corepack and pnpm 10.33
- A supported local platform for Netlify Database
Install dependencies:
corepack enable
pnpm install
Create a local environment file:
cp .env.example .env.local
openssl rand -base64 32
Put the generated value in BETTER_AUTH_SECRET. Better Auth requires a high-entropy secret of at least 32 characters. The local values should look like this:
BETTER_AUTH_SECRET=<generated-secret>
BETTER_AUTH_URL=http://localhost:8888
BETTER_AUTH_ALLOWED_HOSTS=localhost:3000,localhost:8888,127.0.0.1:3000,127.0.0.1:8888
# Optional Slack/community invitation used by the existing site UI.
NEXT_PUBLIC_INVITE_LINK=
Do not add NETLIFY_DB_URL to this file. Netlify supplies it to the application automatically.
Start the app in the first terminal:
pnpm dev
This runs netlify dev, which starts the local Postgres-compatible database and proxies the Next.js server at http://localhost:8888. Keep it running. Directly running pnpm dev:next bypasses the Netlify development environment and therefore does not provide the database connection.
In a second terminal, apply the committed migrations to the running local database:
pnpm db:migrate
pnpm db:status
The local database does not apply migrations automatically. Run pnpm db:migrate after cloning, after pulling migrations, and after generating a new migration. The command only targets the local database by default; Netlify applies remote migrations during deploys.
Environment variables #
| Variable | Where | Purpose |
|---|---|---|
BETTER_AUTH_SECRET |
Local and Netlify | Private, high-entropy secret used by Better Auth for encryption and hashing. Never prefix it with NEXT_PUBLIC_. |
BETTER_AUTH_URL |
Local; Netlify override | Optional explicit canonical fallback. Normal Netlify deploys derive this from Netlify's read-only URL variable. |
BETTER_AUTH_ALLOWED_HOSTS |
Local; Netlify override | Optional comma-separated host allowlist. Setting it replaces the automatic Netlify host list; values do not include URL paths. |
NETLIFY_DB_URL |
Supplied by Netlify | Database connection string selected for the local, preview, or production database branch. Do not commit or manually configure it for normal app execution. |
NEXT_PUBLIC_INVITE_LINK |
Optional | Public community invitation displayed by the site. It is intentionally browser-visible. |
Production and deploy-preview hosts #
Normal Netlify deployments do not need either URL variable. At runtime, the app uses Netlify's read-only URL, SITE_NAME, and SITE_ID values to configure:
- the primary custom or
netlify.apphostname fromURL; - the site's default
SITE_NAME.netlify.apphostname; and - the site-scoped
*--SITE_NAME.netlify.apppattern for deploy previews, branch deploys, and unique deploy URLs.
Better Auth validates the hostname of each request against that list and uses the matching preview hostname dynamically. The URL value is the canonical fallback for request-less server API calls. The build-only DEPLOY_PRIME_URL and DEPLOY_URL hosts are also added when present, but runtime preview support does not depend on them.
Do not use *.netlify.app: Better Auth automatically adds allowed hosts to its trusted origins, so that broad pattern would trust unrelated and potentially hostile Netlify sites.
Set BETTER_AUTH_URL only when an explicit canonical fallback should take precedence over Netlify's URL. Set BETTER_AUTH_ALLOWED_HOSTS for a nonstandard host policy, additional custom-domain aliases, or a custom automatic deploy subdomain. It is an override, so include every required host—including SITE.netlify.app and *--SITE.netlify.app if previews should continue to work.
Schema and migrations #
The Drizzle configuration reads both schemas:
db/auth-schema.tscontains Better Auth's users, sessions, accounts, and verification records.db/schema.tscontains the SacTech event, moderation, and optional recurrence models.
Recurring event rules #
A recurring event has one event_recurrence row whose primary key is the parent event ID. An event without that row is a one-time event. Deleting the event deletes its recurrence row automatically.
| Rule | Persisted constraint |
|---|---|
| Frequency | day, week, month, or year, repeated every interval units; the interval must be from 1 through 99. |
| Weekly | weekdays is required, must contain 1–7 integers, and may contain only 0 (Sunday) through 6 (Saturday). Other frequencies require it to be null. |
| Monthly | monthly_pattern is required and is either day_of_month or nth_weekday. Other frequencies require it to be null. |
| Never ends | end_type=never; both the end date and occurrence count are null. |
| Ends on a date | end_type=on_date; end_date is required and the occurrence count is null. |
| Ends after a count | end_type=after_occurrences; occurrence_count is required from 2 through 1000 and the end date is null. |
Event date/time input and recurrence calculations use the fixed IANA timezone America/Los_Angeles—Pacific time, switching between PST and PDT automatically. The form intentionally has no timezone picker. end_date is a calendar date in that same timezone, while event start and end instants remain timezone-aware timestamps.
Event cancellations #
Event owners can cancel without another moderation decision. Setting event.canceled_at cancels the one-time event or the entire recurring series; canceled_by records the owner who performed the cancellation when that account still exists. Cancellation does not rewrite the approval status, so the moderation history remains intact.
A single occurrence of a recurring event is canceled by inserting its Pacific calendar date into event_occurrence_cancellation. The (event_id, occurrence_date) pair is unique, preventing duplicate exceptions, and deleting the parent event removes all of its occurrence cancellations. occurrence_date is interpreted in America/Los_Angeles, matching recurrence generation and avoiding UTC date shifts near midnight.
After changing either schema, generate a SQL migration:
pnpm db:generate
Review the generated SQL under netlify/database/migrations, then run the app and apply it locally:
# Terminal 1
pnpm dev
# Terminal 2
pnpm db:migrate
Commit the schema change, generated SQL, and Drizzle migration metadata together. Netlify recognizes committed SQL files in netlify/database/migrations and applies them automatically:
- to the isolated database branch before a deploy preview becomes available; and
- to production immediately before the new deploy is published.
A failed migration blocks that deploy. Do not add pnpm db:migrate to the Netlify build command, edit a migration that has already shipped, or use drizzle-kit push against production. Add a new, preferably backwards-compatible migration instead. For breaking changes, use an expand/migrate/contract sequence. See Netlify's migration lifecycle and guidance.
If Better Auth configuration or plugins change the auth model, regenerate its Drizzle schema first, review the result, and then generate the SQL migration:
pnpm auth:generate
pnpm db:generate
Create the first admin #
Apply the auth migrations before bootstrapping an admin. The Better Auth CLI needs a persistent database and NETLIFY_DB_URL in the same shell; it cannot use the connection that exists only inside the separately running Next.js process.
For production, a Team Owner can open Netlify's Database view, select the production branch, and use Copy connection string. Expose that value temporarily as NETLIFY_DB_URL in a trusted shell; do not save it in .env.local or the repository. Under Netlify's current access rules, a Developer receives a read-only production connection string and cannot use it to create the admin.
Once NETLIFY_DB_URL is available in that shell, run:
pnpm auth:create-admin --email admin@example.com --name "SacTech Admin" --role admin
Let the CLI prompt for the password instead of passing --password, which can expose it in shell history or process listings. The CLI creates the account through Better Auth, hashes its password, and marks the email verified by default. Remove a temporarily exported production NETLIFY_DB_URL from the shell when finished.
Alternatively, promote an existing account without handling its password:
- Have the person create their account normally.
- In Netlify, open the project and choose Database.
- Select the correct database branch—use the production branch only when intentionally granting live access—and choose View/edit.
- Open the
usertable, find the exact email address, and change itsrolevalue toadmin. - Have the person sign out and back in before opening
/admin/events.
Changes made through the production database editor take effect immediately. Verify the branch, account, and new role before saving. Under Netlify's current database access rules, only a Team Owner can edit the production branch.
Deploy to Netlify #
- Push the repository, including
netlify.tomland all generated migrations, to the Git provider Netlify will use. - In Netlify, choose Add new project and import the repository. The checked-in configuration installs Chromium for the browser integration tests, runs
pnpm verify, publishes.next, and selects Node 24.19.0. Verification runs formatting, linting, type checking, tests, and one production build, so a failed quality gate blocks the deploy. - Under Project configuration → Environment variables, add
BETTER_AUTH_SECRETand the optionalNEXT_PUBLIC_INVITE_LINK. Configure the values for every deploy context that should support authentication. Netlify supplies the auth URLs and database connection automatically. - Deploy the site.
Because @netlify/database is a project dependency, Netlify uses package-based provisioning: on the first deploy it creates the database if needed, injects the branch-specific NETLIFY_DB_URL, and applies committed migrations as part of the deploy lifecycle. A database does not need to be created manually first. It can still be provisioned from the Netlify Database page if the team prefers to do that before the first deploy.
After the production deploy succeeds:
- Confirm the production database branch and migrations in Netlify's Database view.
- Create or promote the first admin.
- Test account creation, event submission, moderation, and the public calendar.
- Verify a deploy preview separately; it uses an isolated database branch and its own preview hostname.
Scripts #
| Command | Purpose |
|---|---|
pnpm dev |
Start Netlify Dev, the local database, and the Next.js app on port 8888. |
pnpm dev:next |
Start only Next.js on port 3000; useful for UI-only work, but no Netlify database is injected. |
pnpm build |
Create a production Next.js build. |
pnpm start |
Serve an already-built Next.js app. |
pnpm format |
Format supported project files with the pinned Prettier version. |
pnpm format:check |
Check formatting without changing files. |
pnpm lint |
Run ESLint. |
pnpm typecheck |
Run TypeScript without emitting files. |
pnpm test |
Run the Vitest test suite once. |
pnpm test:watch |
Run Vitest in watch mode while developing. |
pnpm verify |
Run the complete CI/deploy gate: formatting, lint, typecheck, tests, and one production build. |
pnpm db:generate |
Generate SQL migrations from the Drizzle schemas. |
pnpm db:migrate |
Apply pending migrations to the running local Netlify database. |
pnpm db:status |
Show local database connection and migration status. |
pnpm auth:generate |
Regenerate Better Auth's Drizzle schema after auth-model changes. |
pnpm auth:create-admin |
Create an initial Better Auth admin when NETLIFY_DB_URL is available. |
Before opening a pull request, run:
pnpm verify
The CI GitHub Actions workflow runs the same command for every pull request and
push to main, using the checked-in Node.js and pnpm versions with a frozen
lockfile. Configure the CI / quality status check as required in the repository's
main branch protection settings. Netlify also installs the test browser and
runs pnpm verify, so direct or manually retried deploys cannot bypass the
checks; the production build inside that command is the single build Netlify
publishes.
Testing #
The test suite is pinned to the Vitest 5 beta requested by the project and is split across two environments:
- React integration tests run in headless Chromium through Vitest Browser Mode's
Playwright provider. They use React Testing Library, DOM Testing Library, and
jest-dom, whilevitest/browserdrives real user interactions through the browser. Tests interact through accessible labels, roles, and visible status messages, with only the network or Server Action boundary mocked. - Server and persistence integration tests run in Node and start an isolated Netlify Database emulator. They apply every committed migration before testing real Drizzle inserts, transactions, authorization decisions, moderation, cancellation, and public-query visibility.
Run the complete suite once with pnpm test, or use pnpm test:watch for fast
feedback while editing. The database-backed files start and stop their own
database, so pnpm dev does not need to be running.
After installing dependencies for the first time, install the Chromium binary
used by Browser Mode with pnpm exec playwright install chromium. CI installs
Chromium and its Linux system dependencies before running the same test suite.
The local Netlify Database emulator uses PGlite and does not reproduce
cross-connection PostgreSQL row-lock blocking. Keep the cancellation concurrency
guard (FOR UPDATE on the parent event) covered in deploy-preview smoke testing,
and use a real Postgres-compatible test database before changing that locking
path.
Security notes #
- Keep
BETTER_AUTH_SECRETandNETLIFY_DB_URLout of Git, logs, screenshots, client components, andNEXT_PUBLIC_*variables. Rotate any value that is exposed. - Keep the Better Auth host allowlist narrow. Add exact custom domains and the site-specific
*--SITE.netlify.apppreview pattern only. - Protect deploy previews appropriately. Preview database branches are isolated, but can contain copied production-shaped data and run server code with preview-scoped environment variables.
- Grant the
adminrole sparingly. Approval and rejection actions mutate public content and are enforced from the server session's role. - Review every migration before committing it and test it on a local database and deploy preview before production.
- The current setup enables email/password sign-up but does not configure an email delivery provider. Add verified-email and password-reset delivery before treating possession of an email address as verified identity.
- Account creation and event submission are intentionally open. Add project-appropriate rate limiting or abuse controls before a high-traffic public launch.
- Event links are restricted to
http://andhttps://, submitted dates are interpreted inAmerica/Los_Angeles, and only approved rows are queried for the public calendar. Preserve those checks when extending the workflow.
Official references #
- Netlify Next.js starter
netlify.toml - Netlify Database local development
- Netlify Database migrations
- Drizzle with Netlify Database
- Better Auth Next.js integration
- Better Auth Drizzle adapter
- Better Auth dynamic base URL
- Better Auth CLI and
create-admin - Better Auth Admin plugin
- Vitest guide
- React Testing Library introduction
- DOM Testing Library installation