# Getting started End-to-end walkthrough: from an empty directory to a running app, then from the bootstrapped template toward (a subset of) the [example app](https://github.com/FSHTech/codegen-example-app). Assumes [setup](setup.md) is done — `fsh`, `uv`, `node`/`yarn`, `just`, and `just-pm` on your PATH. The stack has four generated *sides* driven by jsonnet: a **be** FastAPI backend, a **fe** React/TanStack frontend, an **opa** Rego permissions service, and a **render** Hono microservice. You edit jsonnet (and a few safe-to-edit FE scaffolds); codegen emits the rest. ## Scaffold a new app `fsh template` pulls the codegen template into a directory and starts a fresh git repo (the template's history is dropped, so `origin` doesn't point at it): ```bash fsh template sample cd sample uv sync ``` `uv sync` puts the `codegen` CLI on PATH via the harness `pyproject.toml`. ```{note} The template's deps point at sibling `codegen*` checkouts (editable). `fsh template` auto-detects whether those siblings are present. Force it either way: `--editable` builds from adjacent checkouts; `--no-editable` writes a git-ignored `.env` with `UV_NO_SOURCES=1` so `uv` pulls the published packages from CodeArtifact instead. ``` To run multiple stacks in parallel, bake a port/DB offset in at pull time: ```bash fsh template sample --offset 100 ``` `--offset N` writes `.env` so every dev port shifts by `N` and the app targets the `app_N` database. It's frozen at pull time — changing it later means editing `.env` and re-running DB setup. ## Bootstrap the four sides The freshly-pulled directory holds only the four `bootstrap.jsonnet` files (one per side) plus the harness. `just bootstrap` scaffolds each side into `be/`, `fe/`, `opa/`, `render/`: ```bash just bootstrap ``` It runs the four `*_root` generators (`be_root`, `fe_root`, `opa`, `render_root`), then `just-pm sync` to vendor the shared `just` modules, then a per-side setup + lint-fix pass: ```jsonnet // be/bootstrap.jsonnet — the be_root toggles that decide what gets scaffolded. { name: "codegen-template-app", module: "app", opentelemetry: true, // telemetry block + init_telemetry(app) files: true, // boto3 + fsh_lib.files upload helpers auth: true, // auth.py stubs + auth.jwt(...) block psycopg: true, codegen_database: true, pgqueuer: true, // background worker recipes rate_limit: true, // rate_limit.slowapi(...) block comms: true, // comms platform (requires pgqueuer) fsh: true, // /fsh platform app: saved views, opt-ins opa: true, // OPA gate + per-resource Rego schemas samples: true, // a runnable User + Category CRUD out of the box links: true, // /l/ short-link redirector reports: true, // saved + system reports oauth: true, // "Sign in with Google" (OIDC) } ``` The four sides: - **be** — FastAPI + SQLAlchemy. Walks `be/config/project.jsonnet` → each app's config and emits the API tree under `be/_generated/`. - **fe** — React + TanStack. Parses the BE's `openapi.json` and emits the typed client + hooks under `fe/src/_generated/`. - **opa** — Rego permissions service; rules and per-resource JSON schemas sync from the backend's emitted contract (see [permissions](guides/permissions.md)). - **render** — Hono microservice consuming the BE's render contract (drives PDFs and templated comms output). Every emitted file is `if_exists="skip"`, so re-running `just bootstrap` is a no-op until you pass `--force` to the underlying CLI. To see what a re-run *would* change (templates drift silently under `skip`): ```bash just bootstrap-diff # or: just bootstrap-diff --check (CI gate) ``` ## Generate the contract surface First run — bootstrap then regenerate every contract surface in one shot: ```bash just fresh # = bootstrap + spec ``` After any `*.jsonnet` config edit, regenerate the BE + FE generated code: ```bash just generate # aliases: just g / just gen -> be gen + fe gen just validate # check the be + fe configs against their schemas ``` `just generate` covers in-repo generated code. The downstream *contract* surfaces (OpenAPI, TS client, OPA sync, render regen) have their own recipes: ```bash just spec # BE codegen -> specs/openapi.json -> FE TS client just opa-spec # BE codegen -> opa sync + verify just render-spec # BE codegen -> render regen ``` ```{warning} The FE `yarn openapi-ts` step (which produces the actual TypeScript client) reads `be/specs/openapi.json`, which only exists once the BE has emitted it. `just spec` handles the ordering; if you run the FE client step by hand, make sure the BE spec is fresh first. ``` ## Run it ```bash just dev # alias: just d ``` Runs the BE on `:8000`, the FE on `:5173`, and the pgqueuer worker under one process group (Ctrl-C stops all). For a parallel stack, pass a port offset: ```bash just dev 100 # ports +100, uses the app_100 database ``` Sign in at `http://localhost:5173/login` with `alice / wonderland`. First run needs the DB: `cd be && just db init`. ## From template to example (a walkthrough) A freshly bootstrapped app has empty app config — its tracker app lists `resources: []`, so the BE emits only project-scope plumbing (db wiring, auth routes, the app skeleton). Below we build it toward the [example app](https://github.com/FSHTech/codegen-example-app), each step linking its deep-dive guide. After every config edit, re-run `just generate` (and `just spec` when the FE needs the refreshed client). ### 1. Declare project plumbing in `project.jsonnet` `be/config/project.jsonnet` holds cross-cutting platform features: databases, auth, telemetry, rate-limiting, links, OPA, comms, reports, and the `apps[]` list. Each is a helper import. Databases and `apps[]` are the minimum: ```jsonnet // be/config/project.jsonnet local auth = import "be/auth/jwt.libsonnet"; local db = import "be/db/databases.libsonnet"; local opa = import "be/opa/opa.libsonnet"; local reports = import "be/reports/reports.libsonnet"; local telemetry = import "be/telemetry/telemetry.libsonnet"; { databases: [ db.postgres("primary", { default: true }), ], // ... apps: [ { config: import "tracker.jsonnet", prefix: "/tracker" }, ], } ``` Layer platform features on — each one helper block, reach for the guide when you turn it on: - `auth: auth.config({...})` with `auth.password(...)` / `auth.oauth(...)` methods — [auth](guides/auth.md). - `opa: opa.config({ roles_loader, bindings_loader })` — [permissions](guides/permissions.md). - `comms: comms.platform({...})` — typed messages, user templates, recipient groups — [emails and templates](guides/emails-and-templates.md). - `reports: reports.platform({...})` with `report_fns` — [reports](guides/reports.md). - `telemetry.otel(...)`, `rate_limit.slowapi(...)`, `links.shortener(...)` — each a single block; see [infra](infra.md) for the deployment side. The OPA block is just its two loaders: ```jsonnet // be/config/project.jsonnet — permissions wiring. opa: opa.config({ roles_loader: "permissions.load_roles", bindings_loader: "permissions.load_bindings", }), ``` ### 2. Add an app under a prefix An *app* is a versioned module with a `resources[]` list, mounted under a URL prefix in `apps[]`. The template's app starts empty; the example's `tracker` collects the core resources: ```jsonnet // be/config/tracker.jsonnet { version: "1", module: "tracker", resources: [ import "resources/project.jsonnet", import "resources/task.jsonnet", import "resources/user.jsonnet", import "resources/role.jsonnet", // ... ], } ``` Split large apps into more `{ config, prefix }` entries in `project.jsonnet` (the example adds `inventory`, `purchasing`, `vendors`). See [resources](guides/resources.md) for how apps and resources compose. ### 3. Add your first resource A resource declares its model, primary key, route prefix, one or more *representations* (field projections) and *operations* (CRUD + more). The simplest is the shared name-dimension preset — one line yields full CRUD: ```jsonnet // be/config/resources/role.jsonnet — id + name catalogue, full CRUD. (import "be/resources/presets.libsonnet").nameDimension("fsh.models.Role", "/roles") ``` A hand-rolled resource spells out its representations and operations. The example's project resource shows the shape — representations built from [fields](guides/fields.md) helpers, an [enum](guides/enums.md) field, and a searchable list op with [filters](guides/search-and-filters.md): ```jsonnet // be/config/resources/project.jsonnet (trimmed) local fields = import "be/fields.libsonnet"; local list = import "be/operations/list.libsonnet"; { model: "fsh.models.Project", pk: { name: "id", type: "uuid" }, route_prefix: "/projects", search: {}, representations: [ { name: "list_item", fields: [ fields.id(), { name: "name", type: "str" }, { name: "slug", type: "str" }, { name: "project_type", type: "enum", enum: "fsh.models.ProjectType" }, ], }, // ... ], representation_roles: { default: "default" }, operations: [ { name: "get", representation: "resource" }, list.searchable( representation="list_item", filters=[ { name: "id", values: "self" }, { name: "project_type", values: "enum", enum: "fsh.models.ProjectType" }, ], order=["name"], default_order="name", ), { name: "create", fields: [/* ... */] }, { name: "update", fields: [/* ... */] }, { name: "delete" }, ], } ``` The `enum` field's choices come from a Python `StrEnum` (`fsh.models.ProjectType`) — see [enums](guides/enums.md). The filter kinds (`self`, `ref`, `enum`, `bool`) are covered in [search and filters](guides/search-and-filters.md). ### 4. Add an action and per-op guards Beyond CRUD, resources declare *actions* — named operations backed by a dotted Python callable, optionally gated by a `can` guard. The example's task resource adds a `complete` action plus per-op controls: ```jsonnet // be/config/resources/task.jsonnet (operations, trimmed) local resource = import "be/resources/presets.libsonnet"; operations: [ { name: "create", rate_limit: "10/minute", // per-op override pre: "tracker.hooks.normalise_task_title_on_create", // pre-write hook fields: [/* ... */], }, { name: "delete" }, resource.action( name="complete", fn="tracker.actions.complete_task", // Server-side visibility gate: available_actions omits `complete` // from a row when this returns False — the FE drops it automatically. can="tracker.guards.can_complete_task", ), ], ``` Actions, hooks, and guards are the subject of [actions](guides/actions.md). ### 5. Layer features onto resources With the platform wired in step 1, opt individual resources into each feature: - **Permissions** — set `representation_roles: { opa: "authz" }` so a chosen representation is the projection sent to OPA on each point check ([permissions](guides/permissions.md)): ```jsonnet // be/config/resources/task.jsonnet representation_roles: { default: "default", opa: "authz", }, ``` - **Comms** — a `comms_target` makes the resource a user-template target, its representation the variable surface ([emails and templates](guides/emails-and-templates.md)): ```jsonnet // be/config/resources/task.jsonnet comms_target: comms.target({ representation: "comms_summary", recipient_resolvers: { watchers: "tracker.comms_resolvers.task_watchers", project_lead: "tracker.comms_resolvers.task_project_lead", }, }), ``` - **Reports** — `reports.opt_in(...)` with `report_type` + a `report_fn` source surfaces system/saved reports on the resource ([reports](guides/reports.md)): ```jsonnet // be/config/resources/project.jsonnet reports: reports.opt_in({ types: ["csv", "json"], system_reports: [ reports.system({ slug: "all_projects", name: "All projects", report_type: "csv", source: reports.list_op_source({ base_fn: "project.list" }), }), ], }), ``` [Auditing](guides/auditing.md) and [commenting](guides/commenting.md) layer on the same way (both are not yet merged). ### 6. Wire the frontend Once the BE shape is set, regenerate and fill the safe-to-edit FE scaffolds. The generated client + hooks land under `fe/src/_generated/` (never edited); the scaffolds under `fe/src/{fields,enums,actions}` and `fe/src/routes.ts` are scaffolded once (`if_exists="skip"`) and are yours to fill: ```bash just generate && just spec # emit types + client, then the FE scaffolds ``` - **Field catalogs** ([fields](guides/fields.md)) — one file per resource; give each field a `cell`. It won't type-check until you do: ```typescript // fe/src/fields/tracker/task.tsx import { defineFields } from "@fsh/components-library"; import type { ListItemTaskResource } from "../../_generated/types/tracker/task.gen"; export const taskFields = defineFields()((field) => [ field("title", { header: "Title", cell: (r) => r.title, table: { sortable: true } }), field("completed", { header: "Completed", cell: (r) => r.completed }), ]); ``` - **Enum displays** ([enums](guides/enums.md)) — labels + badge variants, typed against the generated enum so a new backend value fails to compile until you label it: ```typescript // fe/src/enums/projectType.ts import { defineEnumDisplay } from "@fsh/components-library"; import type { ProjectType } from "../_generated/types/tracker/project.gen"; export const projectType = defineEnumDisplay({ product: { label: "Product", variant: "neutral" }, internal: { label: "Internal", variant: "neutral" }, }); ``` - **Action drawers** ([actions](guides/actions.md)) — every entry defaults to `null` (opted out); replace a `null` with `{ label, icon, onAction }` (or a `view` overlay) to surface it: ```typescript // fe/src/actions/tracker/task.tsx export const taskActions = defineResourceActions(taskActionCatalog, { resource: "task", object: { view: null, update: null, delete: null, complete: null }, collection: { view: null, create: null }, bulk: {}, }); ``` - **Routes** — register pages in `fe/src/routes.ts` with `defineArea`, then link them from the sidebar in `Shell.tsx`: ```typescript // fe/src/routes.ts const assets = defineArea({ list: { path: "/assets", lazy: () => import("./pages/assets/AssetListPage") }, detail: { path: "/assets/$id", lazy: () => import("./pages/assets/AssetDetailPage") }, }); const areas = mergeAreas({ assets, categories, comms, settings }); ``` - **Tables** — column ordering, sorting, and defaults are tuned in [table configuration](guides/table-configuration.md). ## Next steps - [resources](guides/resources.md) — apps, models, representations, operations. - [fields](guides/fields.md) — the field helpers and representation shapes. - [enums](guides/enums.md) — backend `StrEnum` → FE displays and filters. - [search and filters](guides/search-and-filters.md) — `list.searchable`, filter kinds, keyset paging. - [actions](guides/actions.md) — actions, hooks, `can` guards, FE drawers. - [permissions](guides/permissions.md) — the OPA gate, roles, bindings, per-resource projections. - [auth](guides/auth.md) — JWT sessions, password + OAuth/OIDC login. - [emails and templates](guides/emails-and-templates.md) — the comms platform and user templates. - [reports](guides/reports.md) — saved + system reports, `report_fn` sources, PDF/CSV/JSON. - [table configuration](guides/table-configuration.md) — FE table columns, sorting, defaults. - [auditing](guides/auditing.md) — change history (not yet merged). - [commenting](guides/commenting.md) — per-record comments (not yet merged). - [infra](infra.md) — deployment, environments, and operational concerns.