Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Copy to .env (read by Next.js, the Prisma CLI and scripts) and fill in. Never commit the real file.
# Only DATABASE_URL, DIRECT_URL and the two JWT secrets are required; everything else degrades gracefully.

# --- Database: Neon Postgres --------------------------------------------------------------------
# Neon console → Connect → enable "Connection pooling" for the pooled URL (host contains "-pooler").
# The app uses the pooled URL at runtime; migrations use the direct (unpooled) one.
DATABASE_URL="postgresql://USER:PASSWORD@ep-xxxx-pooler.REGION.aws.neon.tech/neondb?sslmode=require"
DIRECT_URL="postgresql://USER:PASSWORD@ep-xxxx.REGION.aws.neon.tech/neondb?sslmode=require"
# Only needed for `prisma migrate dev` / drift checks when you can't let Prisma create one
# (e.g. a dedicated Neon branch such as "shadow").
# SHADOW_DATABASE_URL="postgresql://USER:PASSWORD@ep-shadow.REGION.aws.neon.tech/neondb?sslmode=require"
# Driver: "neon" (serverless driver, default for *.neon.tech) or "pg" (node-postgres, default otherwise).
# DATABASE_DRIVER=neon

# Public base URL (used for Google OAuth callback and password-reset links)
APP_URL="http://localhost:3000"

# --- Auth ------------------------------------------------------------------------------------------
# Generate with: node -e "console.log(require('crypto').randomBytes(48).toString('base64url'))"
JWT_SECRET=""
JWT_REFRESH_SECRET=""

# Google OAuth 2.0 (authorised redirect URI: ${APP_URL}/api/auth/google/callback)
GOOGLE_CLIENT_ID=""
GOOGLE_CLIENT_SECRET=""

# --- Integrations -----------------------------------------------------------------------------------
GOOGLE_MAPS_API_KEY=""

# Firebase Realtime Database (GPS ingestion). base64 of the service-account JSON:
# base64 -w0 firebase-service-account.json
FIREBASE_CREDENTIALS_BASE64=""
FIREBASE_DATABASE_URL="https://matatu-eta-default-rtdb.europe-west1.firebasedatabase.app/"

# Africa's Talking SMS ("sandbox" uses the sandbox API)
AFRICAS_TALKING_API_KEY=""
AFRICAS_TALKING_USERNAME="sandbox"

# SMTP for password-reset emails (without these, dev uses an Ethereal test inbox)
EMAIL_HOST="smtp.gmail.com"
EMAIL_PORT=465
EMAIL_SECURE=true
EMAIL_USER=""
EMAIL_PASS=""
EMAIL_FROM=""

# --- Background ETA cycle ------------------------------------------------------------------------------
# interval: run every 30 s inside `next start` (default when not on Vercel)
# cron: a scheduler calls /api/cron/eta-cycle (default on Vercel, see vercel.json)
# off: disabled (e.g. when running `npm run eta:worker` separately)
# ETA_CYCLE_MODE=interval
# Required for /api/cron/eta-cycle (Vercel sends it as `Authorization: Bearer <CRON_SECRET>`)
CRON_SECRET=""
4 changes: 4 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Every schema change needs a maintainer's review (docs/MIGRATIONS.md).
/prisma/ @cliffkoome
/docs/MIGRATIONS.md @cliffkoome
/.github/workflows/ @cliffkoome
98 changes: 98 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
name: CI

on:
pull_request:
push:
branches: [main]

concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: read

env:
NEXT_TELEMETRY_DISABLED: 1

jobs:
checks:
name: Lint, typecheck, unit tests, build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- name: Validate Prisma schema
run: npm run db:validate
- run: npm run lint
- run: npm run typecheck
- run: npm test
- name: Build (no database needed)
run: npm run build

migrations:
name: Migrations (rules, drift, apply, integration tests)
runs-on: ubuntu-latest
services:
postgres:
image: postgres:17
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: matconnect
ports: ["5432:5432"]
options: >-
--health-cmd "pg_isready -U postgres"
--health-interval 5s
--health-timeout 5s
--health-retries 10
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/matconnect
DIRECT_URL: postgresql://postgres:postgres@localhost:5432/matconnect
SHADOW_DATABASE_URL: postgresql://postgres:postgres@localhost:5432/matconnect_shadow
JWT_SECRET: ci-access-secret-0123456789abcdefghijklmnop
JWT_REFRESH_SECRET: ci-refresh-secret-0123456789abcdefghijklmnop
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # the rule checker diffs against the base branch
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci

- name: Create shadow database
run: PGPASSWORD=postgres psql -h localhost -U postgres -c 'CREATE DATABASE matconnect_shadow'

- name: Migration rules (immutability, ordering, destructive SQL)
env:
# PRs compare with their base branch; pushes to main compare with the previous commit.
MIGRATIONS_BASE_REF: ${{ github.event_name == 'pull_request' && format('origin/{0}', github.base_ref) || github.event.before }}
run: npm run db:migrate:check

- name: No drift between schema.prisma and the migrations
run: |
if ! npx prisma migrate diff --from-migrations prisma/migrations --to-schema prisma/schema.prisma --exit-code --script; then
echo "::error::schema.prisma and prisma/migrations disagree. Run 'npm run db:migrate:create' and commit the migration."
exit 1
fi

- name: Apply the full history to an empty database
run: npx prisma migrate deploy

- name: Migrated database matches schema.prisma
run: npx prisma migrate diff --from-config-datasource --to-schema prisma/schema.prisma --exit-code

- name: Re-running deploy is a no-op
run: npx prisma migrate deploy

- name: Integration tests (real Postgres)
run: npm run test:integration

- name: Seed script works
run: npm run db:seed
51 changes: 51 additions & 0 deletions .github/workflows/deploy-migrations.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
name: Deploy migrations

# The ONLY path by which schema changes reach production (docs/MIGRATIONS.md, rule R3).

on:
push:
branches: [main]
paths:
- "prisma/migrations/**"
- "prisma/schema.prisma"
workflow_dispatch:

# Never run two production migrations at once, and never cancel one half-way.
concurrency:
group: deploy-migrations-production
cancel-in-progress: false

permissions:
contents: read

jobs:
deploy:
name: prisma migrate deploy → Neon production
runs-on: ubuntu-latest
environment: production # add required reviewers in Settings → Environments to gate this
env:
# Neon *direct* (unpooled) connection string of the production branch.
DIRECT_URL: ${{ secrets.DATABASE_URL_DIRECT_PRODUCTION }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci

- name: Check configuration
run: |
if [ -z "$DIRECT_URL" ]; then
echo "::error::Set the DATABASE_URL_DIRECT_PRODUCTION secret (Neon direct connection string)."
exit 1
fi

- name: Pending migrations
run: npx prisma migrate status || true

- name: Apply migrations
run: npx prisma migrate deploy

- name: Production matches schema.prisma
run: npx prisma migrate diff --from-config-datasource --to-schema prisma/schema.prisma --exit-code
69 changes: 69 additions & 0 deletions .github/workflows/neon-preview.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
name: Neon preview branch

# For PRs that change the database: copy production into a Neon branch (copy-on-write, seconds) and
# apply the PR's migrations to it, so migrations are tested against real data before merge.
# Does nothing until the NEON_PROJECT_ID variable and NEON_API_KEY secret are configured.

on:
pull_request:
types: [opened, reopened, synchronize, closed]
paths:
- "prisma/**"

concurrency:
group: neon-preview-${{ github.event.number }}
cancel-in-progress: true

permissions:
contents: read

jobs:
migrate-preview:
if: github.event.action != 'closed' && vars.NEON_PROJECT_ID != ''
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci

# Start from a fresh copy of production on every push.
- name: Remove previous preview branch
continue-on-error: true
uses: neondatabase/delete-branch-action@v3
with:
project_id: ${{ vars.NEON_PROJECT_ID }}
branch: preview/pr-${{ github.event.number }}
api_key: ${{ secrets.NEON_API_KEY }}

- name: Create preview branch from production
id: branch
uses: neondatabase/create-branch-action@v5
with:
project_id: ${{ vars.NEON_PROJECT_ID }}
branch_name: preview/pr-${{ github.event.number }}
username: ${{ vars.NEON_DB_USER || 'neondb_owner' }}
database: ${{ vars.NEON_DB_NAME || 'neondb' }}
api_key: ${{ secrets.NEON_API_KEY }}

- name: Apply this PR's migrations to the copy
env:
DIRECT_URL: ${{ steps.branch.outputs.db_url }}
run: npx prisma migrate deploy

- name: Result matches schema.prisma
env:
DIRECT_URL: ${{ steps.branch.outputs.db_url }}
run: npx prisma migrate diff --from-config-datasource --to-schema prisma/schema.prisma --exit-code

cleanup:
if: github.event.action == 'closed' && vars.NEON_PROJECT_ID != ''
runs-on: ubuntu-latest
steps:
- uses: neondatabase/delete-branch-action@v3
with:
project_id: ${{ vars.NEON_PROJECT_ID }}
branch: preview/pr-${{ github.event.number }}
api_key: ${{ secrets.NEON_API_KEY }}
27 changes: 24 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,27 @@
# dependencies
/node_modules

# Next.js
/.next/
/out/
next-env.d.ts
*.tsbuildinfo

# Prisma client (generated by `prisma generate` / postinstall)
/src/generated/

# env files — commit only the example
.env
.env.*
!.env.example

# testing
/coverage

# misc
.DS_Store
*.pem
npm-debug.log*
.vercel
requests.rest
server/config/firebase-service-account.json
/tests
repomix*
repomix*
3 changes: 0 additions & 3 deletions .vscode/settings.json

This file was deleted.

Loading
Loading