Skip to content

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.

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.ts
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 first
const 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 ready
const 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 ready
const 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;

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.

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.

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.

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.

Terminal window
cd examples/shopping_list
sigil up

After startup completes:

  • Postgres is running on port 5433 with the shopping_list database
  • 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.