Skip to content
Open
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
60 changes: 60 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Familiarise local development.
#
# make up everything (Postgres + API + Flutter web)
# make db Postgres only — the fast path if containers feel slow
# make down stop, keeping data and build caches
#
# `make help` lists everything.

.DEFAULT_GOAL := help
.PHONY: help up db api down reset logs shell psql test regen regen-force seed-sql doctor

help: ## Show this help
@grep -hE '^[a-z-]+:.*?## ' $(MAKEFILE_LIST) \
| awk 'BEGIN{FS=":.*?## "}{printf " \033[36m%-12s\033[0m %s\n", $$1, $$2}'

up: ## Start the full stack (Postgres + API + Flutter web)
docker compose up

db: ## Start Postgres only, provisioned — use with a native `dart_frog dev`
docker compose up db-init

api: ## Start Postgres + API, no Flutter web
docker compose up db-init api

down: ## Stop everything, KEEPING the database and build caches
docker compose down

reset: ## Destroy the database and all build caches (next start is a cold rebuild)
docker compose down -v

logs: ## Tail logs from all services
docker compose logs -f

shell: ## Open a shell in the API container
docker compose exec api bash

psql: ## Open psql against the local database
docker compose exec db psql -U familiarise -d familiarise

test: ## Run the backend test suite inside the API container
docker compose exec api dart test

regen: ## Backend codegen, skipped if schema/versions/build.yaml are unchanged
./scripts/regenerate-build.sh --prisma

regen-force: ## Backend codegen, wiping lib/generated first
./scripts/regenerate-build.sh --prisma --force

seed-sql: ## Rebuild backend/prisma/sql/seed-dev.sql from prompts/testing/unit/*.md
./scripts/gen-dev-seed.sh

doctor: ## Check that the local toolchain can run the stack
@printf 'docker : '; docker --version 2>/dev/null || echo 'MISSING'
@printf 'compose : '; docker compose version --short 2>/dev/null || echo 'MISSING'
@printf 'daemon : '; v=$$(docker info --format '{{.ServerVersion}} ({{.Architecture}})' 2>/dev/null); \
[ -n "$$v" ] && [ "$$v" != " ()" ] && echo "$$v" || echo 'NOT RUNNING — start Docker Desktop'
@printf 'flutter : '; flutter --version 2>/dev/null | head -1 || echo 'MISSING'
@printf 'port 5433 : '; lsof -ti:5433 >/dev/null 2>&1 && echo 'IN USE — set FAM_PG_PORT' || echo 'free'
@printf 'port 8080 : '; lsof -ti:8080 >/dev/null 2>&1 && echo 'IN USE' || echo 'free'
@printf 'port 3000 : '; lsof -ti:3000 >/dev/null 2>&1 && echo 'IN USE' || echo 'free'
11 changes: 11 additions & 0 deletions backend/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
.env
.env.*
!.env.example
.dart_tool/
.packages
build/
.git/
.vscode/
.serena/
*.md
test/
32 changes: 27 additions & 5 deletions backend/.env.example
Original file line number Diff line number Diff line change
@@ -1,9 +1,27 @@
# Familiarise Backend Environment Configuration
# Copy this file to .env and fill in the values

# Database (Supabase PostgreSQL)
DATABASE_URL=postgresql://user:password@host:6543/postgres?pgbouncer=true
DIRECT_URL=postgresql://user:password@host:5432/postgres
# Copy this file to .env and fill in the values.
#
# ── Precedence ───────────────────────────────────────────────────────────────
# main.dart resolves configuration in this order, last one winning:
# .env → .env.local → the process environment
# So an exported variable (or a docker compose `environment:` entry) always
# beats this file. That is what makes the database switchable without editing
# anything.
#
# ── Database ─────────────────────────────────────────────────────────────────
# You normally do NOT need to set the two URLs below.
#
# docker compose up → containers get the local Postgres
# source scripts/use-db.sh local → native dart_frog dev, local Postgres
# source scripts/use-db.sh supabase → native, cloud DB (see .env.supabase.example)
#
# Set them here only if you want a fixed default for every native run. Leaving
# a stale cloud URL here is the classic way to run destructive tests against
# shared data by accident — the startup log prints the resolved host on every
# boot so you can catch it.
#
# DATABASE_URL=postgresql://user:password@host:6543/postgres?pgbouncer=true
# DIRECT_URL=postgresql://user:password@host:5432/postgres

# JWT Secret (generate a secure random string)
JWT_SECRET=your-jwt-secret-here
Expand Down Expand Up @@ -36,5 +54,9 @@ STRIPE_SECRET_KEY=sk_test_xxxxx
# Webhook signing secret from Stripe Dashboard -> Webhooks
STRIPE_WEBHOOK_SECRET=whsec_xxxxx

# PAN encryption key (required by the consultant tax-info routes).
# 32 bytes, hex-encoded — generate with: openssl rand -hex 32
PAN_ENCRYPTION_KEY=0000000000000000000000000000000000000000000000000000000000000000

# Server
PORT=8080
25 changes: 25 additions & 0 deletions backend/.env.supabase.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Template for backend/.env.supabase — the shared cloud database.
#
# Copy to backend/.env.supabase (gitignored) and fill in the real values, then:
#
# source scripts/use-db.sh supabase # native dart_frog dev
# FAM_DIRECT_URL="$DIRECT_URL" docker compose up # containers
#
# Local development does NOT need this file. The default everywhere is the
# docker compose Postgres; this exists only for the times you must reproduce
# something against real data.
#
# ⚠️ This is a shared database. Anything you write is visible to everyone and
# anything you delete is gone. Prefer `source scripts/use-db.sh local`.
# backend/prisma/sql/seed-dev.sql and docker/db-init refuse to touch any
# host that is not local, precisely so this cannot be provisioned by
# accident.

# Session-mode pooler (port 5432). The backend uses DIRECT_URL because
# prisma_flutter_connector relies on prepared statements, which PgBouncer's
# transaction mode does not support.
DIRECT_URL=postgresql://USER:PASSWORD@aws-0-REGION.pooler.supabase.com:5432/postgres

# Transaction-mode pooler (port 6543). Kept for parity with the deployed
# configuration; the backend prefers DIRECT_URL when both are set.
DATABASE_URL=postgresql://USER:PASSWORD@aws-0-REGION.pooler.supabase.com:6543/postgres?pgbouncer=true
13 changes: 12 additions & 1 deletion backend/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,13 @@
# Files and directories created by pub
.dart_tool/
.packages
pubspec.lock
# NOTE: pubspec.lock is deliberately NOT ignored. This is an application
# package (publish_to: none), so the lockfile belongs in version control —
# without it Docker builds and CI resolve fresh every time, which makes them
# non-reproducible and lets a caret-range bump (e.g. freezed 2.x -> 3.x, a
# breaking generator change) break the build with no code change. It is also
# an input to backend/scripts/ensure-generated.sh's codegen hash.
# The root pubspec.lock has always been tracked; this makes the two consistent.

# Files and directories created by dart_frog
build/
Expand All @@ -16,7 +22,12 @@ build/
coverage/

# Environment files
# .env.supabase holds real cloud credentials; .env.local is the per-developer
# override layer. Only the *.example templates are tracked.
.env
.env.*
!.env.example
!.env.*.example

# ============================================
# Code Generation (regenerate with commands below)
Expand Down
82 changes: 82 additions & 0 deletions backend/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# Familiarise Mobile API — Production Dockerfile
#
# IMPORTANT BUILD NOTES (learned from failed deployments):
#
# 1. BASE IMAGE: Must use Flutter image, NOT dart:stable.
# prisma_flutter_connector depends on the Flutter SDK for dependency
# resolution. Using dart:stable causes "flutter from sdk doesn't exist"
# during `dart pub get`.
#
# 2. PRISMA CODEGEN: lib/generated/ is gitignored (PR #108), so Railway
# never receives the Prisma-generated types. The Dockerfile must regenerate
# them via `dart run prisma_flutter_connector:generate`.
#
# 3. BUILD_RUNNER: After Prisma generation, freezed/json_serializable must
# also run. Without this, the AOT compile fails with "Not a constant
# expression" and undefined type errors in repositories.
#
# 4. PRISMA SCHEMA: backend/prisma/schema.prisma was originally a symlink
# to ../../familiarise_web/prisma/schema.prisma. Docker cannot follow
# symlinks that point outside the build context. The symlink was replaced
# with a real file copy. If the web schema changes, re-copy it:
# cp ~/Desktop/familiarise_web/prisma/schema.prisma backend/prisma/schema.prisma
#
# 5. RUNTIME IMAGE: Cannot use `FROM scratch` with the Flutter build image.
# The official dart:stable image provides /runtime/ (libc + friends) for
# scratch-based final stages, but the Flutter image does not. AOT-compiled
# Dart binaries need libc, so we use debian:bookworm-slim instead.
#
# 6. LOCAL TESTING: Always test locally before deploying to Railway:
# docker build -t familiarise-mobile-api .
# docker run -p 8080:8080 --env-file .env familiarise-mobile-api
# curl http://localhost:8080/api/health
#
# Build steps mirror: scripts/regenerate-build.sh --backend
# ─────────────────────────────────────────────────────────────

# ── Stage 1: Build ──────────────────────────────────────────
FROM ghcr.io/cirruslabs/flutter:stable AS build

WORKDIR /app

# Install dart_frog_cli for the build step
RUN dart pub global activate dart_frog_cli
ENV PATH="/root/.pub-cache/bin:${PATH}"

# Copy and resolve dependencies first (Docker layer caching)
COPY pubspec.* ./
RUN flutter pub get

# Copy full source
COPY . .
RUN flutter pub get --offline

# 1. Generate Prisma client (lib/generated/ is gitignored, must regenerate)
RUN dart run prisma_flutter_connector:generate \
--schema prisma/schema.prisma \
--output lib/generated \
--server

# 2. Run build_runner for freezed/json codegen on generated models
RUN dart run build_runner build --delete-conflicting-outputs

# 3. Generate Dart Frog build output (creates build/bin/server.dart)
RUN dart_frog build

# 4. AOT compile the generated server to a native binary
RUN dart compile exe build/bin/server.dart -o build/bin/server

# ── Stage 2: Runtime ────────────────────────────────────────
# Use debian:bookworm-slim (NOT scratch) — the Flutter build image does not
# provide /runtime/ like dart:stable does. AOT binaries need libc + friends.
FROM debian:bookworm-slim

RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*

COPY --from=build /app/build/bin/server /app/bin/server

EXPOSE 8080

CMD ["/app/bin/server"]
Comment on lines +72 to +82

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Run both API containers as a non-root user. Both images retain the default root user, so a compromise of the Dart Frog process has root privileges inside its container.

  • backend/Dockerfile#L72-L82: create an unprivileged runtime user, copy the server with appropriate ownership, and switch to it before CMD.
  • backend/Dockerfile.dev#L16-L43: create and switch to an unprivileged user while ensuring named-volume initialization remains writable.
🧰 Tools
🪛 Checkov (3.3.8)

[low] 1-82: Ensure that HEALTHCHECK instructions have been added to container images

(CKV_DOCKER_2)


[low] 1-82: Ensure that a user for the container has been created

(CKV_DOCKER_3)

🪛 Hadolint (2.14.0)

[warning] 74-74: Pin versions in apt get install. Instead of apt-get install <package> use apt-get install <package>=<version>

(DL3008)

📍 Affects 2 files
  • backend/Dockerfile#L72-L82 (this comment)
  • backend/Dockerfile.dev#L16-L43
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/Dockerfile` around lines 72 - 82, Run both API containers as non-root
users. In backend/Dockerfile lines 72-82, create an unprivileged runtime user,
copy the server with ownership assigned to that user, and switch to it before
CMD. In backend/Dockerfile.dev lines 16-43, create and switch to an unprivileged
user while preserving writable named-volume initialization.

Source: Linters/SAST tools

43 changes: 43 additions & 0 deletions backend/Dockerfile.dev
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Development image for the Dart Frog API — hot reload, used by docker-compose.
#
# This is NOT the deployment image. See ./Dockerfile for that; its header
# comments document the constraints both images share, in particular:
#
# The base MUST be a Flutter image, not dart:stable. prisma_flutter_connector
# declares `flutter: sdk: flutter`, so `pub get` fails on a Dart-only SDK with
# "flutter from sdk doesn't exist".
#
# Deliberately does NO code generation at build time. lib/generated is ~735k
# lines rebuilt from prisma/schema.prisma; baking it into a layer would mean
# re-running it on every image rebuild. Instead docker-compose keeps
# lib/generated and .dart_tool in named volumes and the entrypoint regenerates
# only when the inputs actually change (see backend/scripts/ensure-generated.sh).

FROM ghcr.io/cirruslabs/flutter:stable

WORKDIR /app

# curl: used by the compose healthcheck.
# postgresql-client: lets you psql the database from inside the container.
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
ca-certificates \
postgresql-client \
&& rm -rf /var/lib/apt/lists/*

RUN dart pub global activate dart_frog_cli
ENV PATH="/root/.pub-cache/bin:${PATH}"

# Copied to /usr/local/bin rather than left in /app, because compose
# bind-mounts ./backend over /app and would shadow anything placed there.
COPY docker/dev-entrypoint.sh /usr/local/bin/dev-entrypoint
RUN chmod +x /usr/local/bin/dev-entrypoint

EXPOSE 8080

ENTRYPOINT ["/usr/local/bin/dev-entrypoint"]

# --hostname 0.0.0.0 is required for the published port to be reachable from
# the host; dart_frog dev binds localhost otherwise. Verified supported in
# dart_frog_cli 1.2.14 (it must be a literal IP, not a hostname).
CMD ["dart_frog", "dev", "--hostname", "0.0.0.0", "--port", "8080"]
40 changes: 40 additions & 0 deletions backend/build.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Backend code-generation scope.
#
# ── Why this file exists ─────────────────────────────────────────────────────
#
# json_serializable was running as a build phase over every Dart file in this
# package and producing NOTHING. There is not a single `part '*.g.dart'`
# directive under backend/lib or backend/routes, and no generated .freezed.dart
# references _$…FromJson — the Prisma connector emits hand-written
# fromJson/toJson bodies instead (see lib/generated/models/*.dart).
#
# The one .g.dart in the package, lib/generated/schema_registry.g.dart, is
# emitted by `prisma_flutter_connector:generate`, NOT by build_runner, so
# disabling json_serializable cannot affect it.
#
# Disabling it also removes two more phases for free: json_serializable
# `applies_builders: [source_gen|combining_builder]`, and combining_builder is
# `auto_apply: none` (so nothing else pulls it in) and itself applies
# source_gen|part_cleanup. It additionally drops freezed's
# `runs_before: [json_serializable]` ordering barrier, allowing more parallelism.
#
# If you ever add an @JsonSerializable class to the backend, set `enabled: true`
# below and delete this comment.

targets:
$default:
builders:
json_serializable:json_serializable:
enabled: false

freezed:freezed:
enabled: true
# Every @freezed class in this package is Prisma-generated. Scoping
# freezed to lib/generated stops it walking routes/ (132 files) and the
# hand-written parts of lib/ looking for annotations that cannot be
# there.
generate_for:
include:
- lib/generated/**.dart
exclude:
- lib/generated/**.freezed.dart
25 changes: 25 additions & 0 deletions backend/docker/dev-entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#!/usr/bin/env bash
# Entrypoint for the API dev container.
#
# Runs before `dart_frog dev`. Everything here is designed to be a no-op on a
# warm container so `docker compose restart api` comes back in seconds rather
# than minutes.

set -euo pipefail
cd /app
export PATH="/root/.pub-cache/bin:$PATH"

# .dart_tool is a named volume, so it starts empty on a fresh volume even
# though pubspec.yaml is bind-mounted from the host. Resolve if needed.
if [ ! -f .dart_tool/package_config.json ]; then
echo "==> flutter pub get (cold volume)"
flutter pub get
fi

# Hash-guarded: regenerates lib/generated only when schema.prisma, the
# connector/freezed versions, or build.yaml actually changed. On a warm
# container this prints "up to date" and exits immediately.
./scripts/ensure-generated.sh

echo "==> starting: $*"
exec "$@"
Loading