infra(infrastructure): 🧱 Configure local TLS certs, Docker Compose for platform services (DB, MinIO, mail, Caddy), DB schema initialization, and CI type-checking workflow setup

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
autocommit 2026-05-16 21:48:03 -07:00
parent bf2df14f07
commit bf1e394ec0
7 changed files with 327 additions and 0 deletions

View file

@ -0,0 +1,33 @@
name: typecheck
on:
push:
branches: [main]
paths:
- '@platform/**/*.ts'
- '@platform/**/*.tsx'
- '@platform/**/tsconfig*.json'
- '@platform/package.json'
- '@platform/bun.lock'
workflow_dispatch:
jobs:
typecheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
lfs: false # never fetch the .archive/*.tar.zst blobs in CI
- name: setup bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: install
working-directory: '@platform'
run: bun install --frozen-lockfile
- name: typecheck
working-directory: '@platform'
run: bun run typecheck

View file

@ -0,0 +1,132 @@
# Local development reverse proxy — atlilith V3
#
# Resolves *.atlilith.apricot.lan domains to their Vite dev servers.
# Run: caddy run --config @platform/infrastructure/Caddyfile.local
#
# Uses internal TLS (mkcert) for .lan domains.
#
# Adding a new dev subdomain → add it to gen-local-certs.sh DOMAINS array
# (or include in the wildcard), add a server block below with `import local_tls`.
{
# auto_https off — TLS is explicit per-site via mkcert certs.
http_port 80
https_port 443
default_bind 0.0.0.0 ::
auto_https off
}
# Unified mkcert wildcard for all *.atlilith.apricot.lan dev hosts.
# Regenerate the cert: @platform/scripts/gen-local-certs.sh
(local_tls) {
tls /var/home/lilith/Code/@projects/@atlilith/@platform/infrastructure/certs/_wildcard.atlilith.apricot.lan.crt /var/home/lilith/Code/@projects/@atlilith/@platform/infrastructure/certs/_wildcard.atlilith.apricot.lan.key
}
# HTTP → HTTPS redirect for all .atlilith.apricot.lan domains
:80 {
redir https://{host}{uri} 301
}
# ─── Provider sites (per-instance) ─────────────────────────────────────────
# Quinn's instance keeps the quinn.apricot.lan hostnames during cutover;
# new providers get {provider}.atlilith.apricot.lan.
https://atlilith.apricot.lan {
import local_tls
# Marketing landing (Vite on :5220)
handle {
reverse_proxy 127.0.0.1:5220 {
header_up Host {host}
}
}
}
# ─── Provider portal (generic) ─────────────────────────────────────────────
https://portal.atlilith.apricot.lan {
import local_tls
handle {
reverse_proxy 127.0.0.1:5274 {
header_up Host {host}
}
}
}
# ─── AI assistant ──────────────────────────────────────────────────────────
https://ai.atlilith.apricot.lan {
import local_tls
handle {
reverse_proxy 127.0.0.1:5276 {
header_up Host {host}
}
}
}
# ─── Messenger ─────────────────────────────────────────────────────────────
https://m.atlilith.apricot.lan {
import local_tls
handle {
reverse_proxy 127.0.0.1:5275 {
header_up Host {host}
}
}
}
# ─── Admin (platform-wide) ─────────────────────────────────────────────────
https://admin.atlilith.apricot.lan {
import local_tls
handle {
reverse_proxy 127.0.0.1:5221 {
header_up Host {host}
}
}
}
# ─── SSO (auth) ────────────────────────────────────────────────────────────
https://sso.atlilith.apricot.lan {
import local_tls
handle {
reverse_proxy 127.0.0.1:3045 {
header_up Host {host}
}
}
}
# ─── API gateway (Hono) ────────────────────────────────────────────────────
https://api.atlilith.apricot.lan {
import local_tls
handle {
reverse_proxy 127.0.0.1:3050 {
header_up Host {host}
}
}
}
# ─── Analytics (org-analytics) ─────────────────────────────────────────────
https://data.atlilith.apricot.lan {
import local_tls
# SSO auth gate (DEV_MODE: SSO always returns 200, transparent passthrough).
@protected not path /analytics/track/*
forward_auth @protected localhost:3045 {
uri /auth/validate
@unauthed status 401
handle_response @unauthed {
redir https://sso.atlilith.apricot.lan/login?redirect=https://{host}{uri} 302
}
}
# Public ingest path (write-key authenticated by collector).
handle /analytics/track/* {
reverse_proxy 127.0.0.1:4201 {
header_up X-Write-Key "dev-write-key"
}
}
# Dashboard SPA
handle {
reverse_proxy 127.0.0.1:5211 {
header_up Host {host}
}
}
}

View file

@ -0,0 +1,22 @@
services:
platform-db:
image: postgres:16-alpine
container_name: atlilith-platform-db
environment:
POSTGRES_DB: platform
POSTGRES_USER: platform
POSTGRES_PASSWORD: devpassword
ports:
- "25440:5432"
volumes:
- atlilith-platform-db-data:/var/lib/postgresql/data
- ./platform-db-init.sql:/docker-entrypoint-initdb.d/000_init.sql:ro
- ./sql/migrations:/docker-entrypoint-initdb.d/migrations:ro
healthcheck:
test: ["CMD-SHELL", "pg_isready -U platform -d platform"]
interval: 5s
timeout: 3s
retries: 5
volumes:
atlilith-platform-db-data:

View file

@ -0,0 +1,20 @@
services:
platform-mail:
image: axllent/mailpit:latest
container_name: atlilith-platform-mail
ports:
- "1026:1025"
- "8026:8025"
environment:
MP_MAX_MESSAGES: 5000
MP_DATABASE: /data/mailpit.db
volumes:
- atlilith-platform-mail-data:/data
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:8025/api/v1/info > /dev/null || exit 1"]
interval: 10s
timeout: 5s
retries: 5
volumes:
atlilith-platform-mail-data:

View file

@ -0,0 +1,21 @@
services:
platform-minio:
image: minio/minio:latest
container_name: atlilith-platform-minio
command: server /data --console-address ":9101"
environment:
MINIO_ROOT_USER: platform-dev
MINIO_ROOT_PASSWORD: devpassword
ports:
- "9100:9000"
- "9101:9101"
volumes:
- atlilith-platform-minio-data:/data
healthcheck:
test: ["CMD", "mc", "ready", "local"]
interval: 5s
timeout: 3s
retries: 5
volumes:
atlilith-platform-minio-data:

View file

@ -0,0 +1,45 @@
#!/usr/bin/env bash
# Generate mkcert certificates for all *.atlilith.apricot.lan dev subdomains.
# Run once on a new machine or when adding a new subdomain.
# Requires: mkcert (brew install mkcert / apt install mkcert)
#
# Usage:
# bash @platform/scripts/gen-local-certs.sh # generate missing
# bash @platform/scripts/gen-local-certs.sh --force # regenerate all
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
CERTS_DIR="$REPO_ROOT/@platform/infrastructure/certs"
FORCE=false
for arg in "$@"; do [[ "$arg" == "--force" ]] && FORCE=true; done
if ! command -v mkcert &>/dev/null; then
echo "ERROR: mkcert not found. Install it first:" >&2
echo " Linux: brew install mkcert OR sudo apt install mkcert" >&2
echo " macOS: brew install mkcert" >&2
exit 1
fi
mkdir -p "$CERTS_DIR"
# Single wildcard cert covers all subdomains.
WILDCARD_CRT="$CERTS_DIR/_wildcard.atlilith.apricot.lan.crt"
WILDCARD_KEY="$CERTS_DIR/_wildcard.atlilith.apricot.lan.key"
if [[ "$FORCE" == "false" && -f "$WILDCARD_CRT" && -f "$WILDCARD_KEY" ]]; then
echo " [skip] wildcard cert already exists at $WILDCARD_CRT"
else
echo " [gen] wildcard cert for *.atlilith.apricot.lan + atlilith.apricot.lan"
mkcert \
-cert-file "$WILDCARD_CRT" \
-key-file "$WILDCARD_KEY" \
"atlilith.apricot.lan" "*.atlilith.apricot.lan"
fi
echo ""
echo "Certs at: $CERTS_DIR"
echo ""
echo "Next: (re)start Caddy to pick up changes:"
echo " manage-apps restart atlilith.proxy apricot"

View file

@ -0,0 +1,54 @@
-- atlilith V3 — base schema bootstrap.
-- Runs once when the docker postgres volume is fresh.
-- Migrations in sql/migrations/ run AFTER this (alphabetical by docker-entrypoint).
CREATE SCHEMA IF NOT EXISTS platform;
CREATE SCHEMA IF NOT EXISTS analytics;
CREATE SCHEMA IF NOT EXISTS messenger;
CREATE SCHEMA IF NOT EXISTS booking;
CREATE SCHEMA IF NOT EXISTS content;
CREATE SCHEMA IF NOT EXISTS ops;
-- Common extensions
CREATE EXTENSION IF NOT EXISTS "pgcrypto"; -- gen_random_uuid()
CREATE EXTENSION IF NOT EXISTS "citext"; -- case-insensitive text (emails)
-- Tenancy core: users table (Person tenant).
-- Migration 001_add_orgs.sql adds the Org overlay.
CREATE TABLE IF NOT EXISTS users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
slug TEXT UNIQUE NOT NULL CHECK (slug ~ '^[a-z][a-z0-9-]{1,62}[a-z0-9]$'),
email CITEXT UNIQUE NOT NULL,
display_name TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
-- Updated_at maintenance (reused by org tables in migration 001)
CREATE OR REPLACE FUNCTION touch_updated_at() RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = now();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS trg_users_updated_at ON users;
CREATE TRIGGER trg_users_updated_at
BEFORE UPDATE ON users
FOR EACH ROW EXECUTE FUNCTION touch_updated_at();
-- Seed: transquinnftw (inaugural Person tenant; Cocotte org seeded in 002).
INSERT INTO users (slug, email, display_name)
VALUES ('transquinnftw', 'booking@transquinnftw.com', 'Quinn')
ON CONFLICT (slug) DO NOTHING;
-- Placeholder analytics_events table referenced by migration 001
-- (real definition comes from the org-analytics feature later).
CREATE TABLE IF NOT EXISTS analytics_events (
id BIGSERIAL PRIMARY KEY,
event_name TEXT NOT NULL,
user_id UUID NULL REFERENCES users(id),
occurred_at TIMESTAMPTZ NOT NULL DEFAULT now()
);