Add supabase stack + general compose runner

Adds "a stack whose default is a substrate": a shared self-hosted Supabase
backend plus a `--stack supabase` that scaffolds per-app projects (migrations +
edge functions + static UI) which deploy against it. Apps own their code and
stay repo-durable; only their rows/blobs live on the shared substrate.

- compose runner: new RunCompose supervises a multi-container stack as one
  systemd unit (ExecStart=`compose up`, generated ExecStop=`compose down`);
  secrets via EnvironmentFile. Reusable beyond Supabase. Deployment.stop_cmd
  carries the teardown command through the registry.
- supabase stack: CLI --stack choice, STACK_DEFAULTS→frontend with
  build.outputs=[public], _scaffold_supabase() (migrations/RLS/functions/
  static UI/app manifest), and SupabaseHandler with a forward-only idempotent
  migration runner (plan_migrations + psql build) and deno dev-verbs.
- docs: docs/stacks/supabase.md, registered in CLAUDE.md; compose runner
  documented in registry.md.
- tests: compose run/stop cmd, systemd ExecStop, compose host-route TLS,
  migration planner, supabase verb resolution, create --stack supabase.
This commit is contained in:
2026-06-30 18:18:47 -07:00
parent fc5802d823
commit b726e79c23
19 changed files with 822 additions and 10 deletions

View File

@@ -7,6 +7,7 @@ import subprocess
from castle_cli.config import REPOS_DIR, load_config, save_config
from castle_cli.manifest import (
BuildSpec,
CaddySpec,
DefaultsSpec,
ExposeSpec,
@@ -26,6 +27,15 @@ STACK_DEFAULTS: dict[str, str] = {
"python-fastapi": "daemon",
"python-cli": "tool",
"react-vite": "frontend",
"supabase": "frontend",
}
# Static build output per stack, for `behavior: frontend` programs. The gateway
# serves this dir in place at /<name>/ (no service, no process). A supabase app
# ships a raw `public/`; react-vite builds to `dist/`.
STACK_BUILD_OUTPUTS: dict[str, str] = {
"supabase": "public",
"react-vite": "dist",
}
@@ -85,12 +95,19 @@ def run_create(args: argparse.Namespace) -> int:
# Initialize a git repo for the new source.
subprocess.run(["git", "init", "-q", str(project_dir)], check=False)
# Frontend stacks with a static output get a build spec so the gateway emits
# an in-place static route at /<name>/ (no service, no process).
build = None
if stack in STACK_BUILD_OUTPUTS:
build = BuildSpec(outputs=[STACK_BUILD_OUTPUTS[stack]])
config.programs[name] = ProgramSpec(
id=name,
description=description,
source=str(project_dir),
stack=stack,
behavior=behavior,
build=build,
)
if behavior == "daemon":
prefix = name.replace("-", "_").upper()
@@ -122,7 +139,11 @@ def run_create(args: argparse.Namespace) -> int:
print(" Registered in castle.yaml")
print("\nNext steps:")
print(f" cd {project_dir}")
if stack:
if stack == "supabase":
print(" # edit migrations/, functions/, public/ — targets the shared substrate")
print(f" castle program build {name} # apply migrations to the substrate")
print(f" castle deploy && castle gateway reload # serve at /{name}/")
elif stack:
print(" uv sync")
if behavior == "daemon":
print(f" uv run {name} # starts on port {port}")