Full-Stack Example
This guide walks through the examples/shopping_list/ project that ships with Sigil. It’s a shopping list app with three layers: a Postgres database, a Python FastAPI backend, and a React frontend.
Project Structure
Section titled “Project Structure”shopping_list/├── sigil.config.ts # Sigil environment definition├── db/│ ├── schema.sql # Table definitions│ └── seed.sql # Sample data├── backend/│ ├── main.py # FastAPI application│ └── requirements.txt└── frontend/ ├── src/App.tsx # React shopping list UI ├── package.json └── vite.config.tsThe Config File
Section titled “The Config File”import { Environment, Postgres, Service, APIInterface, BrowserInterface } from "sigil";import path from "path";
const root = import.meta.dir;const env = new Environment("shopping-list");
// 1. Database — starts firstconst db = env.add( new Postgres("shopping-db", { port: 5433, database: "shopping_list", username: "postgres", password: "postgres", version: "16", initSql: path.join(root, "db/schema.sql"), seedSql: path.join(root, "db/seed.sql"), }));
// 2. Backend — starts after DB is readyconst backend = env.add( new Service( "backend", { command: [ path.join(root, "backend/.venv/bin/uvicorn"), "main:app", "--host", "0.0.0.0", "--port", "8000", ], cwd: path.join(root, "backend"), env: { DATABASE_URL: db.connectionString, }, readyCheck: { url: "http://localhost:8000/health", interval: 1000, timeout: 30000, }, }, [new APIInterface(8000)] ));
// 3. Frontend — starts after backend is readyconst frontend = env.add( new Service( "frontend", { command: ["bun", "run", "dev"], cwd: path.join(root, "frontend"), env: { VITE_API_URL: "http://localhost:8000", }, readyCheck: { url: "http://localhost:3000", interval: 1000, timeout: 30000, }, }, [new BrowserInterface(3000)] ));
export default env;Key Patterns
Section titled “Key Patterns”Dependency wiring via connection strings
Section titled “Dependency wiring via connection strings”The Postgres entity exposes db.connectionString which returns postgresql://postgres:postgres@localhost:5433/shopping_list. This is injected into the backend’s environment variables:
env: { DATABASE_URL: db.connectionString,}The backend reads DATABASE_URL from its environment and connects to the database. No hardcoded connection details in the application code.
Schema initialization and seeding
Section titled “Schema initialization and seeding”initSql: path.join(root, "db/schema.sql"), // CREATE TABLE ...seedSql: path.join(root, "db/seed.sql"), // INSERT INTO ...Sigil runs initSql first (for DDL), then seedSql (for test data) — both executed inside the Docker container after Postgres reports ready.
Ready checks
Section titled “Ready checks”Each service has a readyCheck that polls an HTTP endpoint:
readyCheck: { url: "http://localhost:8000/health", interval: 1000, // poll every second timeout: 30000, // give up after 30s}Sigil won’t move to the next entity until the current one passes its ready check. This ensures the frontend doesn’t start before the backend is actually serving requests.
Interfaces as metadata
Section titled “Interfaces as metadata”Attaching interfaces to services is declarative metadata about what the service exposes:
[new APIInterface(8000)] // "this service exposes an HTTP API on port 8000"[new BrowserInterface(3000)] // "this service exposes a web UI on port 3000"This is currently used for status reporting and will be used for future features like automatic API discovery and browser automation.
Running It
Section titled “Running It”cd examples/shopping_listsigil upAfter startup completes:
- Postgres is running on port 5433 with the
shopping_listdatabase - FastAPI backend is serving on
http://localhost:8000 - React frontend is serving on
http://localhost:3000
Open http://localhost:3000 to see the shopping list UI.