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
16 changes: 16 additions & 0 deletions .github/workflows/web-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,19 @@ jobs:

- name: Build
run: pnpm build

- name: Install Playwright browsers
run: pnpm playwright install --with-deps chromium

- name: E2E tests
run: pnpm test:e2e
env:
AUTH_SECRET: ci-test-secret-not-used-in-prod
NEXT_PUBLIC_FIREBASE_API_KEY: placeholder
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN: placeholder.firebaseapp.com
NEXT_PUBLIC_FIREBASE_PROJECT_ID: placeholder
FIREBASE_PROJECT_ID: placeholder
FIREBASE_SERVICE_ACCOUNT_JSON: '{"type":"service_account"}'
BACKEND_URL: http://localhost:8080
NEXT_PUBLIC_BACKEND_URL: http://localhost:8080
NEXT_TELEMETRY_DISABLED: 1
34 changes: 34 additions & 0 deletions RUNBOOK.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Operational guide for deploying and maintaining the fullstack template in stagin
- [Prerequisites](#prerequisites)
- [Environment variables](#environment-variables)
- [First-time production setup](#first-time-production-setup)
- [Pre-launch checklist](#pre-launch-checklist)
- [Deploying the backend](#deploying-the-backend)
- [Deploying the web app](#deploying-the-web-app)
- [Deploying the mobile app](#deploying-the-mobile-app)
Expand Down Expand Up @@ -121,6 +122,39 @@ See [Deploying the backend](#deploying-the-backend).

---

## Pre-launch checklist

Run through this before going live with any project based on this template.

### Configuration
- [ ] `ENV` is set to `production` — enables JSON logging and disables `/debug/pprof` and `/admin/queues`
- [ ] `AUTH_SECRET` is a strong random value: `openssl rand -base64 32`
- [ ] `CORS_ALLOWED_ORIGINS` lists your exact domain(s) — never `*` in production
- [ ] `BLUEPRINT_DB_SSLMODE` is `require`
- [ ] `RATE_LIMIT_RPS` is configured to a sensible value for your expected traffic

### Infrastructure
- [ ] Database backups are enabled on your PostgreSQL host (Supabase, Neon, and RDS all have this in their dashboard — turn it on)
- [ ] Run `make migrate-up` against the production database before the first deploy, and on every subsequent deploy that includes migrations
- [ ] If using R2 file uploads, CORS rules on the R2 bucket allow requests from your web domain

### Firebase
- [ ] Firebase Authentication is enabled with only the sign-in methods your app uses
- [ ] Firebase security rules are reviewed (default rules may be too permissive)
- [ ] `FIREBASE_SERVICE_ACCOUNT_JSON` in production is the production project's key, not a dev key

### Observability
- [ ] `SENTRY_DSN` (backend) and `NEXT_PUBLIC_SENTRY_DSN` (web) are set — you want errors reported from day one
- [ ] The `/health` endpoint returns `200` after deploying (confirms DB connectivity)
- [ ] Grafana is accessible and the **Backend Overview** dashboard shows live data (`http://<host>:3001`)

### Mobile (if shipping Android)
- [ ] `google-services.json` in the release build points to the production Firebase project
- [ ] Release APK/AAB is signed with the production keystore (not the debug keystore)
- [ ] Staged rollout is configured in Google Play Console before full release

---

## Deploying the backend

The backend compiles to a single binary. Choose one deployment model:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ apiVersion: 1

datasources:
- name: Prometheus
uid: prometheus
type: prometheus
access: proxy
url: http://prometheus:9090
Expand Down
52 changes: 52 additions & 0 deletions web/e2e/auth.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { test, expect } from '@playwright/test'

test.describe('auth guard', () => {
Comment thread
GRACENOBLE marked this conversation as resolved.
test('redirects /dashboard to /login when unauthenticated', async ({ page }) => {
await page.goto('/dashboard')
await expect(page).toHaveURL(/\/login/)
})

test('redirects /settings to /login when unauthenticated', async ({ page }) => {
await page.goto('/settings')
await expect(page).toHaveURL(/\/login/)
})
})

test.describe('login page', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/login')
})

test('renders email field, password field, and sign in button', async ({ page }) => {
await expect(page.getByLabel('Email')).toBeVisible()
await expect(page.getByLabel('Password')).toBeVisible()
await expect(page.getByRole('button', { name: /sign in/i })).toBeVisible()
})

test('shows link to register page', async ({ page }) => {
await expect(page.getByRole('link', { name: /sign up/i })).toBeVisible()
})

test('shows validation errors on empty submit', async ({ page }) => {
await page.getByRole('button', { name: /sign in/i }).click()
await expect(page.getByText('Please enter a valid email address')).toBeVisible()
await expect(page.getByText('Password is required')).toBeVisible()
})
})

test.describe('register page', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/register')
})

test('renders name, email, password, and confirm password fields', async ({ page }) => {
await expect(page.getByLabel('Name')).toBeVisible()
await expect(page.getByLabel('Email')).toBeVisible()
await expect(page.getByLabel('Password', { exact: true })).toBeVisible()
await expect(page.getByLabel('Confirm password')).toBeVisible()
})

test('shows link back to login page', async ({ page }) => {
await expect(page.getByRole('link', { name: 'Sign in' })).toBeVisible()
})
})
4 changes: 3 additions & 1 deletion web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
"lint": "eslint",
"test": "vitest run",
"test:watch": "vitest",
"test:ui": "vitest --ui"
"test:ui": "vitest --ui",
"test:e2e": "playwright test"
},
"dependencies": {
"@hookform/resolvers": "^5.4.0",
Expand Down Expand Up @@ -38,6 +39,7 @@
"zod": "^4.4.3"
},
"devDependencies": {
"@playwright/test": "^1.61.1",
"@tailwindcss/postcss": "^4",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
Expand Down
25 changes: 25 additions & 0 deletions web/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { defineConfig, devices } from '@playwright/test'

export default defineConfig({
testDir: './e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
reporter: 'list',
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
],
webServer: {
command: 'pnpm dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
timeout: 120_000,
},
})
55 changes: 47 additions & 8 deletions web/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions web/vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export default defineConfig({
environment: 'jsdom',
setupFiles: ['./vitest.setup.ts'],
globals: true,
exclude: ['**/node_modules/**', '**/e2e/**'],
},
resolve: {
alias: {
Expand Down
Loading