DocsWorking on the code
Repository layout
What every directory at the root is for, including the dotfile ones a tree view usually hides. If you have just cloned this and want to know where something lives, start here.
skelet/
├── apps/ the applications and the Signals module - everything else supports these
├── templates/ the storefronts a shop is forked from
├── packages/ the TypeScript the storefronts share
├── proto/ the core-to-Signals wire contract, owned by neither side
├── cmd/skelet/ the skelet command line - the dev front door, and the binary releases ship
├── docs/ you are here
├── env/ environment templates, one file per service
├── deploy/ sidecar config, one directory per service
├── .github/ CI workflows and dependency bumps
The ones that hold the product
apps/
The applications, and the only place product code lives.
apps/backend/ | Everything server-side, three Go modules side by side and the dev Dockerfile they share |
apps/backend/core/ | The Go GraphQL API. Also the worker and media processes - one module, several binaries under cmd/ |
apps/backend/signals/ | The Signals suite as its own Go module - the signals and ingest processes. It requires core by relative path, never the other way round |
apps/backend/sdk/ | The plugin SDK as its own Go module - what plugin repositories build against, beside core’s gqlgen model packages for the resolvers they add |
apps/dashboard/ | The React admin panel (Vite) |
apps/channels/ | One directory per shop of the project’s own, each forked from a template with skelet add channel <name>. Empty in skelet itself |
Two of these are optional: skelet setup asks which apps you want and removes the
rest from your working tree via sparse-checkout, so a clone that brings its own
frontend can run headless. The backend always runs.
Inside apps/backend/core you will also find untracked build output - server and
tmp/ are a compiled binary and air’s scratch directory, not source; the same
goes for signals and ingest under apps/backend/signals. What the packages inside each app mean is
architecture.md.
templates/
The storefronts a shop is forked from, each named for its framework. nextjs-vetrina is
the full Next.js one, and the one the dev stack runs in skelet itself - for the browser
tests, CI and demo work. astro-shop is a fast Astro one for small catalogs.
They’re upstream’s, like packages/: a project leaves them alone and builds its
shop in apps/channels/, so merging a skelet release never collides with it.
packages/
TypeScript every storefront shares, as source - Next and Vite compile workspace
packages themselves, so there’s no build step here. Each release also publishes
them to npm, compiled, for a shop outside a Skelet checkout - see
releasing. storefront-core is a
storefront’s logic with no framework in it - the GraphQL client and operations,
auth, cart, checkout, channel and locale - and a template builds its design on
it through an adapter: storefront-next for Next, storefront-astro for Astro.
storefront-react holds the client hooks any React framework can use.
The storefronts, the templates and these are one pnpm workspace: the root
pnpm-workspace.yaml lists them and pins the versions they share in its catalog,
so a package and the app using it resolve the same React, and the one root
pnpm-lock.yaml covers them all. The dashboard stays a project of its own.
proto/
The wire contract between core and Signals, as protobuf. It sits at the root
rather than inside either module because neither side owns it - core calls it
and Signals serves it, and both import the code generated from here, which lands
in core’s signalsapi/v1, under the hand-written vocabulary, so the contract is
one directory the Signals module can reach.
buf.yaml holds the lint and compatibility rules, buf.gen.yaml says which
plugins run and where the output lands (apps/backend/core/signalsapi/v1/,
never hand-edited). skelet gen regenerates; skelet check contracts
refuses a change that would break a deployed peer, since the two processes
deploy separately and an old core will meet a new Signals.
cmd/skelet/
The skelet command line: every dev verb, and the binary each release ships,
which installs and starts projects - inside one it builds and runs that project’s
own copy of this directory. It’s the tooling that drives the repo rather than part
of the product, so it sits at the root, not under apps/. Its own Go module, standard library only, and it
imports nothing from the backend - cmd/skelet because Go names a binary after
its directory, so go install gives you skelet.
The ones that hold configuration
env/
Environment variables, split per service rather than one giant file. The rule is
visible in the directory listing: *.env.example is committed, the matching
*.env is git-ignored and is what actually gets read. skelet setup copies the
templates on first run.
garage.toml is the exception - object storage config that carries no secrets,
so it is committed as-is. Every variable, which file it belongs in, and the
tradeoffs behind the optional ones: env.md.
Secrets that apply install-wide (the session and link keys, SIGNALS_SERVICE_SECRET,
CONFIG_ENCRYPTION_KEY, DB_PASSWORD) live in the root .env, from .env.example.
deploy/
Config for sidecar containers, kept out of apps/ because it belongs to no
application. Production shapes only - the dev stack mounts nothing from here.
One directory per service configured, never per deployment shape - which stack mounts a file is a fact about the compose file, and putting it in the path would have sibling directories answering two different questions.
| Read by | |
|---|---|
caddy/ | The bundled reverse proxy: three vhosts, automatic Let’s Encrypt. Behind a profile because it takes :80 and :443. Small and prod |
skelet check contracts reads the Caddyfile directly - it holds one of the three
copies of the security headers that have to agree.
The vhost map in the Caddyfile is meant to be edited - it names the demo storefront rather than deriving yours.
changelog/
One file per release, changelog/0.2.0.md and on, with CHANGELOG.md at the root as
their index. Pending entries wait in changelog/unreleased/ as one small file each, so
branches never edit the same lines, and a release gathers them into the next version’s
file - the naming rule is in that folder’s README.
Root files
docker-compose.yml is the dev stack. docker-compose.small.yml is the
minimum deployable tenant, and docker-compose.prod.yml the full split-tier
shape; both are described under Deploying in the README. pnpm-workspace.yaml and
pnpm-lock.yaml are the storefront workspace’s (see packages/ above). .nvmrc pins Node and is what CI reads (node-version-file),
.gitattributes locks every text file to LF so a Windows checkout cannot rewrite
line endings, .gitignore and .dockerignore keep build contexts and commits
clean. .skelet/ is git-ignored scratch for the CLI - the build cache it falls back to
when your user cache directory isn’t writable, among other things - delete it whenever
you like. README.md, AGENTS.md
and CHANGELOG.md are the only pages up here - everything else belongs in docs/.
Never edit the tracked compose files to add your own service. Docker Compose
auto-merges a git-ignored docker-compose.override.yml, which is where a shop
of your own belongs, so pulling from upstream stays conflict-free.
The ones that support the work
docs/
One file per question you might be asking, and the index that decides which is
which lives in the README rather than here. Some of these are generated rather
than written - permissions.md, settings.md and webhooks.md are rebuilt by
skelet gen from the code that defines them, so edit the source, not the page.
Paths in these pages. A backticked path starting with apps/, templates/,
packages/, deploy/ or docs/ is from the repo root. A bare internal/…, graph/… or
cmd/… is relative to apps/backend/core, which is where all three of those live -
the short form keeps Go prose readable, and there is no second internal/ to
confuse it with. Write the repo-root form when a page compares apps, the way
architecture.md does for the GraphQL surfaces.
.github/
workflows/ci.yml and workflows/security.yml run the same gates you run
locally, deriving their app matrices from apps/channels/, templates/ and
packages/ so a new shop or package is covered without touching a workflow. workflows/release.yml publishes a release,
CLI binaries included, when its tag is pushed in Skelet’s own repository - a project’s tags publish nothing.
renovate.json5 configures dependency bumps and needs no per-app entry either - Renovate scans the repo for manifests
itself, so the same rule holds there. A project keeps it to its own apps/channels/ (workflow.md).
