[READ-ONLY] Mirror of https://github.com/crutchcorn/sac-tech-site. Hi Mark
TypeScript 80%
CSS 20%
JavaScript <1%
<1%

README.md

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:

  1. A person creates an account or signs in at /auth.
  2. From /account, they open /events/submit and send an event for review. New events always start as pending, and the account page tracks their status.
  3. A SacTech admin reviews the queue at /admin/events and approves or rejects each event.
  4. Only approved events are returned to the public /events calendar.

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 #

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 (.nvmrc pins 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.app hostname from URL;
  • the site's default SITE_NAME.netlify.app hostname; and
  • the site-scoped *--SITE_NAME.netlify.app pattern 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.ts contains Better Auth's users, sessions, accounts, and verification records.
  • db/schema.ts contains 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:

  1. Have the person create their account normally.
  2. In Netlify, open the project and choose Database.
  3. Select the correct database branch—use the production branch only when intentionally granting live access—and choose View/edit.
  4. Open the user table, find the exact email address, and change its role value to admin.
  5. 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 #

  1. Push the repository, including netlify.toml and all generated migrations, to the Git provider Netlify will use.
  2. 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.
  3. Under Project configuration → Environment variables, add BETTER_AUTH_SECRET and the optional NEXT_PUBLIC_INVITE_LINK. Configure the values for every deploy context that should support authentication. Netlify supplies the auth URLs and database connection automatically.
  4. 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:

  1. Confirm the production database branch and migrations in Netlify's Database view.
  2. Create or promote the first admin.
  3. Test account creation, event submission, moderation, and the public calendar.
  4. 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, while vitest/browser drives 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_SECRET and NETLIFY_DB_URL out of Git, logs, screenshots, client components, and NEXT_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.app preview 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 admin role 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:// and https://, submitted dates are interpreted in America/Los_Angeles, and only approved rows are queried for the public calendar. Preserve those checks when extending the workflow.

Official references #