> ## Documentation Index
> Fetch the complete documentation index at: https://fymo.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Architecture Tour

> A package-by-package tour of the fymo/ source tree, for anyone about to read or modify the framework itself.

This page is a map, not a manual. It's for someone who just cloned the repo and is about to start grepping through `fymo/`, not for someone building an app on top of it. Each section below names a package's job in a sentence or two and points at the files that make it real. The source and the other concept pages cover the rest.

At the top level, `fymo/` splits into nine packages, listed in the table below. There's also a small `utils` package for shared helpers, like the CLI's color output, not interesting enough for its own section.

## The build/serve split

Fymo draws a hard line between build time and runtime. The build package reads your app/ folder and produces dist/, a self-contained bundle of compiled assets. That includes hashed client bundles, prebuilt SSR modules, a Node sidecar script, and a manifest describing it all.

core never touches app/templates/ or esbuild directly. It only reads from dist/. If the sidecar script is missing, FymoApp refuses to start rather than quietly falling back to some slower path.

<Note>
  This is why `fymo build` is a required step before `fymo serve`. And it's why `fymo dev` runs its own build loop (in `build/dev_orchestrator.py`) instead of recompiling on every request.
</Note>

## Package map

| Package     | Responsibility                                                                                                                                                                          |
| ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `build`     | Discovers routes and layouts from `app/templates/`, generates esbuild entry points, shells out to Node for the actual bundling, and writes `dist/manifest.json`.                        |
| `core`      | The WSGI application (`FymoApp`), request routing and dispatch, the Node sidecar client, middleware (rate limiting, body caps, security headers), and shared SSR controller invocation. |
| `remote`    | The RPC layer: discovers `app/remote/*.py` functions, generates the typed `$remote` client, and dispatches `POST /_fymo/remote/<hash>/<fn>` at runtime.                                 |
| `auth`      | Identity resolvers (`@identify`, auto-discovered from `app/auth/*.py`), `current_uid()`, `@require_auth`, and route-level `require_auth:` enforcement.                                  |
| `jobs`      | Background task registry (`app/jobs/*.py`) and the pluggable `JobProvider` interface (threaded by default, Postgres/Procrastinate for production).                                      |
| `broadcast` | SSE channel registry (`app/broadcasts/*.py`) and the `/_fymo/broadcast/<module>/<channel>` streaming endpoint.                                                                          |
| `storage`   | A `StorageProvider` abstraction for files referenced by `storage.expose` routes, built from `fymo.yml`'s `storage:` section.                                                            |
| `cli`       | The `fymo` command group: `new`, `init`, `dev`, `build`, `serve`, `jobs-worker`.                                                                                                        |
| `server`    | The two ways a built app actually gets served: a threaded dev server and a gunicorn launcher for production.                                                                            |

## Build pipeline

`build/discovery.py` walks `app/templates/` and turns each file into a route or layout object. An `index.svelte` or `show.svelte` file becomes one route. Any `_layout.svelte` file becomes a layout, whether it sits at the root or inside a resource folder.

`build/pipeline.py`'s `BuildPipeline` class takes that output and drives the rest of the pipeline, generating entry files and handing them to a Node build script as a subprocess. It passes a JSON config in and reads a JSON result back over stdout. The result gets matched against the discovered routes and written into `dist/manifest.json`, the versioned contract that core reads at runtime.

```python theme={null}
# fymo/build/pipeline.py
class BuildPipeline:
    """Orchestrates: discover -> generate entries -> invoke esbuild -> write manifest."""
```

`build/hygiene.py` runs before any of that, catching a few mistakes that would otherwise fail silently. It keeps `app/controllers/` Python-only, and the templates and components folders frontend-only. A misplaced file wouldn't raise an error on its own: esbuild just ignores a stray Python file, and Python never imports a stray Svelte file. This check exists so a developer actually sees the mistake, instead of chasing a silent no-op later.

It also checks that a `media:` config always comes with a matching `storage:` config. And any function in `app/remote/*.py` meant for the browser must carry an explicit `@remote` marker.

## Core server and sidecar

`core/server.py` defines `FymoApp`, the WSGI callable everything else plugs into. Its `__init__` method wires everything together. It resolves the identity secret, sets up config and logging, builds the router, and optionally sets up auth. It always initializes the job and broadcast providers, then starts the Node sidecar.

Each request gets timed and access-logged. Then it's dispatched through a chain of checks: body-cap and rate-limit limits, security headers, and routing by path prefix for auth, remote calls, data fetches, and broadcasts. Anything left over falls through to a normal SSR page render.

`core/sidecar.py` is the other half of the runtime story, a persistent Node child process that speaks length-prefixed JSON frames over stdin and stdout. Python sends a render request with the route and its props, and gets back the rendered body and head. It restarts the child on a broken pipe, and enforces a per-call timeout so a hung render can't wedge a worker forever.

`core/ssr_controller.py` holds the shared logic that both the full-page render and the soft-nav data endpoint call into. It's what invokes a controller's `getContext()` and `getDoc()`, so the two paths can't drift apart on how auth context gets opened.

## Remote functions

`remote/discovery.py` finds every type-annotated public function in `app/remote/*.py`, and hashes each module's contents into a short module hash. That hash becomes part of the call URL (`POST /_fymo/remote/<hash>/<fn>`), so a stale client talking to a rebuilt server gets a 404 instead of calling the wrong code.

`remote/router.py`'s `handle_remote` function is the runtime half of this story. It rejects non-POST and cross-origin requests, resolves the hash back to a module, and decodes the incoming payload. Then it validates arguments against the function's signature and runs the call in a request scope. It always replies with a 200, wrapping either a result or an error in the body.

<Tip>
  The 200-always envelope is deliberate. It keeps the generated `$remote` client's error handling in one place, parsing the JSON and branching on `type`, instead of also branching on HTTP status.
</Tip>

## Auth

Fymo owns no user model, only the mechanism. `auth/identity.py` holds the resolver chain: `@identify` registers a function that turns a request into an `Identity(uid)` or `None`, and `current_uid()` walks that chain once per request, caching the result. `auth/discovery.py` auto-imports every module under `app/auth/*.py` so a project's own resolvers self-register, the same glob pattern `remote/discovery.py` uses for `app/remote/*.py`, but a separate one, neither ever scans the other's files.

`auth/context.py` is where `@require_auth` lives: it wraps a remote function and raises `AuthRequired`, a `RemoteError` the router serializes to a 401. `auth/public.py` holds `@public_identity`, the one projection an app registers to decide what crosses to the client as the `$auth` store. Route-level enforcement (`require_auth:` in `fymo.yml`) is handled separately, in `core/page_auth.py`, since it runs before SSR starts rather than inside a remote-function call.

Login and signup don't return a `Set-Cookie` header directly. They queue one through `fymo.remote`'s `set_cookie`/`clear_cookie`, and the router drains that queue after the call returns. `fymo generate auth` scaffolds a real, editable implementation of all of this into `app/auth/` and `app/remote/auth.py`, it's the fastest way to see the pieces fit together.

## Jobs and broadcasts

`jobs/discovery.py` and `broadcast/discovery.py` both discover a registry of functions from an app subdirectory. Jobs live in `app/jobs/*.py`, and broadcasts live in `app/broadcasts/*.py`. Both discovery modules are thin wrappers over the same shared walker in `core/app_discovery.py`.

That walker used to be duplicated between the two, and the copies had quietly diverged. One raised on a duplicate name, the other let the last module win. The collision policy is now an explicit argument the shared function forces every caller to pass.

Jobs get submitted by name to a `JobProvider`, threaded by default. Broadcasts are addressed by channel name through `publish()`, and streamed to the browser at `/_fymo/broadcast/<module>/<channel>`. A channel's own function body doubles as its subscribe-time authorization guard.

## Storage

`storage/registry.py` builds a single `StorageProvider`. It reads that provider's config from the `storage:` section of `fymo.yml`. Unlike jobs or broadcasts, storage has no default.

An app with `storage.expose` entries but no storage provider configured fails at build time, and again at startup. That's safer than quietly writing files to local disk in dev, only to lose them once the app runs behind more than one instance in production.

## CLI and serving

`cli/main.py` defines the `fymo` command group. `new` scaffolds a project, and `init` adds Fymo to an existing one. `build` runs the build pipeline, and `dev` runs a file-watching dev loop.

`serve` starts the app, a dev server by default or gunicorn with `--prod`. And `jobs-worker` runs whichever `JobProvider`'s worker loop is configured.

`server/dev.py` and `server/gunicorn.py` are the two places serve hands off to. The dev server is threaded, unlike a plain `wsgiref` server, which is single-threaded and would let one slow request, or an open SSE subscription, block everyone else.

Production goes through gunicorn instead, and the handoff is trickier than it looks. Each gunicorn worker is a forked process, and the Node sidecar's pipe is a plain file descriptor. A naive fork would let worker siblings inherit and share that pipe, corrupting the frame protocol.

It avoids this by stopping the sidecar before any worker forks, then building a fresh, worker-owned `FymoApp` in a `post_fork` hook. Every worker ends up talking to its own sidecar process.

## Where to go next

<CardGroup cols={2}>
  <Card title="The app/ directory" icon="folder-tree" href="/app-directory">
    A map of where everything lives in a Fymo project.
  </Card>

  <Card title="Remote functions" icon="arrow-right-arrow-left" href="/remote-functions">
    How app/remote/\*.py functions become the typed \$remote client.
  </Card>

  <Card title="Controllers and routing" icon="signpost" href="/controllers-and-routing">
    How a request path resolves to a controller and a template.
  </Card>

  <Card title="Storage and media" icon="folder-open" href="/storage-and-media">
    Configuring a StorageProvider and media: routes.
  </Card>
</CardGroup>
