diff --git a/.air.toml b/.air.toml new file mode 100644 index 0000000..845e4f5 --- /dev/null +++ b/.air.toml @@ -0,0 +1,35 @@ +# Air — Go live reload configuration +# Documentation: https://github.com/air-verse/air + +root = "." +testdata_dir = "testdata" +tmp_dir = "tmp" + +[build] + # The command Air runs to rebuild when files change. + cmd = "go build -o ./tmp/ratify-server ./cmd/server" + # The binary Air runs after a successful build. + bin = "./tmp/ratify-server" + # Files that trigger a rebuild when changed. + include_ext = ["go"] + # Directories Air ignores. + exclude_dir = ["frontend", "tmp", "vendor", "testdata"] + # Kill the running process before rebuilding. + kill_delay = "200ms" + # Show the build log when a rebuild is triggered. + log = "build-errors.log" + +[log] + # Show timestamps in Air's log output. + time = true + +[color] + # Colour the different log levels differently. + main = "magenta" + watcher = "cyan" + build = "yellow" + runner = "green" + +[misc] + # Delete tmp dir on exit. + clean_on_exit = true \ No newline at end of file diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..251271c --- /dev/null +++ b/.env.example @@ -0,0 +1,57 @@ +# ============================================================================= +# Ratify — Environment Configuration +# ============================================================================= +# Copy this file to .env and fill in your values: +# cp .env.example .env +# +# The defaults below work for local development with docker compose. +# Never commit your .env file — it contains real secrets. +# ============================================================================= + +# ----------------------------------------------------------------------------- +# Application +# ----------------------------------------------------------------------------- +PORT=8080 +ENVIRONMENT=development + +# ----------------------------------------------------------------------------- +# Ratify's own metadata database +# This is NOT the database Ratify monitors — it is the database Ratify +# uses to store contracts, proposals, audit logs, and credentials. +# ----------------------------------------------------------------------------- +DATABASE_URL=postgresql://ratify:ratify@postgres:5432/ratify?sslmode=disable + +# ----------------------------------------------------------------------------- +# Security +# ENCRYPTION_KEY: used to encrypt database credentials at rest (AES-256). +# Must be exactly 64 hex characters (32 bytes). +# Generate a new one with: openssl rand -hex 32 +# +# JWT_SECRET: used to sign session tokens for the web UI. +# Generate a new one with: openssl rand -hex 32 +# +# The values below are safe defaults for local development only. +# Use real random values in any other environment. +# ----------------------------------------------------------------------------- +ENCRYPTION_KEY=0000000000000000000000000000000000000000000000000000000000000000 +JWT_SECRET=local-dev-jwt-secret-change-this-in-production + +# ----------------------------------------------------------------------------- +# Email (SMTP) +# Ratify sends notifications when proposals are raised and breaches detected. +# For local development, you can use a free service like Mailtrap.io +# which captures emails without sending them. +# ----------------------------------------------------------------------------- +SMTP_HOST=smtp.mailtrap.io +SMTP_PORT=587 +SMTP_USERNAME= +SMTP_PASSWORD= +SMTP_FROM=ratify@example.com + +# ----------------------------------------------------------------------------- +# Breach detection +# How often Ratify compares live schemas against active contracts. +# Format: Go duration string — 1h, 30m, 24h +# Default is 1 hour. For local development 5m is easier to test with. +# ----------------------------------------------------------------------------- +BREACH_DETECTION_INTERVAL=1h \ No newline at end of file diff --git a/.gitignore b/.gitignore index aaadf73..4082bd7 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,11 @@ go.work.sum # Editor/IDE # .idea/ # .vscode/ + +# Docker and Air live reload +tmp/ +.air.toml.bak + +# Frontend build output and dependencies +frontend/dist/ +frontend/node_modules/ \ No newline at end of file diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..3b79c02 --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,24 @@ +# ============================================================================= +# Ratify — Development overrides +# Use this with: docker compose -f docker-compose.yml -f docker-compose.dev.yml up +# +# Changes from the base docker-compose.yml: +# - Uses Dockerfile.dev instead of Dockerfile (live reload via Air) +# - Mounts source code as a volume so changes are reflected immediately +# - Enables more verbose logging +# ============================================================================= + +services: + app: + build: + context: . + dockerfile: docker/Dockerfile.dev + volumes: + # Mount the entire project into the container. + # Air watches this directory and recompiles on changes. + - .:/app + # Prevent the container's Go module cache from being + # overwritten by the host's directory. + - /app/tmp + environment: + ENVIRONMENT: development \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..6919078 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,77 @@ +# ============================================================================= +# Ratify — Docker Compose +# Starts the Ratify server and its PostgreSQL database. +# +# Usage: +# docker compose up — start everything (production image) +# docker compose up --build — rebuild and start +# docker compose down — stop and remove containers +# docker compose down -v — stop and remove containers + database volume +# +# For active development with live reload: +# docker compose -f docker-compose.yml -f docker-compose.dev.yml up +# ============================================================================= + +services: + + # --------------------------------------------------------------------------- + # PostgreSQL — Ratify's metadata database + # Stores contracts, proposals, teams, audit logs, and encrypted credentials. + # This is NOT the database Ratify monitors — it is Ratify's own database. + # --------------------------------------------------------------------------- + postgres: + image: postgres:16-alpine + restart: unless-stopped + environment: + POSTGRES_USER: ratify + POSTGRES_PASSWORD: ratify + POSTGRES_DB: ratify + ports: + # Expose on localhost so you can connect with a database client + # (e.g. TablePlus, DBeaver, psql) during local development. + # Remove this in production — the app container connects internally. + - "5432:5432" + volumes: + # Persist database data across container restarts. + # Without this, all data is lost when the container stops. + - postgres_data:/var/lib/postgresql/data + healthcheck: + # Wait for PostgreSQL to be ready before starting the app. + # The app container will not start until this passes. + test: ["CMD-SHELL", "pg_isready -U ratify -d ratify"] + interval: 5s + timeout: 5s + retries: 10 + start_period: 10s + + # --------------------------------------------------------------------------- + # Ratify app — the compiled server binary + # Runs migrations on startup, then starts the HTTP server. + # --------------------------------------------------------------------------- + app: + build: + context: . + dockerfile: docker/Dockerfile + restart: unless-stopped + ports: + - "${PORT:-8080}:8080" + env_file: + # Load all environment variables from .env + # Copy .env.example to .env and fill in real values + - .env + environment: + # Override DATABASE_URL so the app connects to the postgres + # service by its service name, not localhost. + DATABASE_URL: postgresql://ratify:ratify@postgres:5432/ratify?sslmode=disable + depends_on: + postgres: + # Only start the app after PostgreSQL health check passes. + condition: service_healthy + +# ============================================================================= +# Volumes +# Named volumes persist data across container restarts. +# ============================================================================= +volumes: + postgres_data: + driver: local \ No newline at end of file diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..78c670f --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,72 @@ +# ============================================================================= +# Ratify — Production Dockerfile +# Two-stage build: compile in a full Go environment, run in a minimal image. +# Final image size: under 50MB. +# ============================================================================= + +# ----------------------------------------------------------------------------- +# Stage 1: Builder +# Uses the official Go image to compile the binary. +# This stage is discarded after the binary is copied to the runtime stage. +# ----------------------------------------------------------------------------- +FROM golang:1.24-alpine AS builder + +# Install ca-certificates and git. +# ca-certificates: needed for HTTPS connections inside the binary. +# git: needed by go mod download for some dependencies. +RUN apk add --no-cache ca-certificates git + +WORKDIR /build + +# Copy dependency files first. +# Docker caches this layer separately from source code. +# If go.mod and go.sum have not changed, Docker reuses the cache +# and skips re-downloading all dependencies on the next build. +COPY go.mod go.sum* ./ +RUN go mod download + +# Copy the rest of the source code. +COPY . . + +# Build the server binary. +# CGO_ENABLED=0 produces a statically linked binary with no C dependencies. +# This is required for the binary to run in the minimal runtime image. +# -ldflags="-w -s" strips debug symbols — reduces binary size significantly. +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \ + -ldflags="-w -s" \ + -o ratify-server \ + ./cmd/server + +# ----------------------------------------------------------------------------- +# Stage 2: Runtime +# Minimal image — only the compiled binary and its dependencies. +# No Go compiler, no source code, no package manager. +# ----------------------------------------------------------------------------- +FROM alpine:3.20 + +# Install ca-certificates in the runtime image. +# Required for the binary to make outgoing HTTPS connections. +RUN apk add --no-cache ca-certificates tzdata + +WORKDIR /app + +# Copy the compiled binary from the builder stage. +COPY --from=builder /build/ratify-server . + +# Copy migration files. +# The server runs migrations on startup, so they must be present. +COPY --from=builder /build/migrations ./migrations + +# Copy email templates. +COPY --from=builder /build/templates ./templates + +# The application listens on this port. +# Must match the PORT environment variable. +EXPOSE 8080 + +# Run as a non-root user for security. +# Never run application containers as root in production. +RUN addgroup -S ratify && adduser -S ratify -G ratify +USER ratify + +CMD ["./ratify-server"] \ No newline at end of file diff --git a/docker/Dockerfile.dev b/docker/Dockerfile.dev new file mode 100644 index 0000000..fb20507 --- /dev/null +++ b/docker/Dockerfile.dev @@ -0,0 +1,24 @@ +# ============================================================================= +# Ratify — Development Dockerfile +# Uses Air for live reload — the server restarts automatically when +# Go files change. Mount the source code as a volume for this to work. +# ============================================================================= + +FROM golang:1.24-alpine + +# Install air (live reload) and other development tools. +RUN apk add --no-cache ca-certificates git curl && \ + go install github.com/air-verse/air@latest + +WORKDIR /app + +# Copy dependency files and download dependencies. +COPY go.mod go.sum* ./ +RUN go mod download + +# The source code is mounted as a volume at runtime (see docker-compose.yml). +# We do not copy it here — air watches the mounted directory directly. + +EXPOSE 8080 + +CMD ["air", "-c", ".air.toml"] \ No newline at end of file diff --git a/templates/.gitkeep b/templates/.gitkeep new file mode 100644 index 0000000..e69de29