Your App's Structure
A fresh project is small. Most of the tree is setup you touch once; the day-to-day folders are app/, config/, and harness/test/.
.
├── app
├── bin
│ ├── build-binary
│ ├── build-image
│ ├── compose
│ ├── dev
│ ├── myapp.rb
│ └── test
├── compose.yml
├── config
│ ├── application.rb
│ ├── boot.rb
│ ├── codegen.rb
│ ├── database.rb
│ ├── environment.rb
│ └── routes.rb
├── deploy
│ └── Caddyfile
├── Dockerfile
├── .dockerignore
├── .env.sample
├── gen
│ ├── controllers.rb
│ ├── forms.rb
│ ├── helpers.rb
│ ├── jobs.rb
│ ├── lib.rb
│ ├── mailers.rb
│ ├── model_columns.rb
│ ├── models.rb
│ ├── route_helpers.rb
│ └── services.rb
├── harness
│ ├── boot.rb
│ ├── test
│ │ ├── up_test.rb
│ │ └── up_test.rb.expected
│ └── test.env
├── main.rb
├── pins.toml
└── spin.tomlThe folders you edit#
app/holds your controllers, models, forms, mailers, jobs, services, helpers, and shared lib classes. A fresh project starts with an empty folder;--scaffoldfills it with the posts-and-comments example.app/services/is for plain classes that wrap a domain operation;app/helpers/is for modules shared by controllers;app/lib/is for shared modules and plain classes. All three load automatically and have no framework base class.config/shapes the app.routes.rbis the route table;application.rbholds values;environment.rbholds mode-dependent wiring;database.rbselects the adapter;boot.rbandcodegen.rbdecide what loads.harness/test/holds one plain Ruby file per case file, with a.expectedsibling.bin/testruns them against a real server.
The files that pin the app together#
main.rbis the compile root. It requiresconfig/boot.rb, which starts the runtime and ends at the app's own files.pins.tomlrecords the runtime release and the compiler revision. The first command that needs the toolchain assembles the one this file names.spin.tomlis the build manifest. It names the allocator and any build-level settings the compiler needs.compose.ymlis the single-host deployment file.Dockerfilebuilds the image;deploy/Caddyfileis the optional TLS proxy..env.sampleis a template for environment variables, not configuration. Copy it to.envand source it; the app does not read the file itself..dockerignorekeeps the build context small.storage/holds the SQLite database and is gitignored.
The generated files#
gen/ is build output. The require-list files carry a # scan: spec and a literal list for one app folder; snails refreshes them before every build. gen/route_helpers.rb and gen/model_columns.rb are generated outright from the route table and the schema. Do not edit any of them by hand — a file added under app/ appears after the next bin/dev or bin/test.
snails-core/ and .spinel/ are local state assembled by sync: the vendored runtime and the pinned compiler. Both are gitignored, so a fresh clone rebuilds them on the first command that needs them.
The exception is a placeholder file in a fresh project. gen/model_columns.rb stays a stub until a schema exists, and the first build tells you to run snails migrate dump when a model needs it.
harness/build/ is also generated and gitignored. It holds the compiled test bundle and the schema loader cache.
A scaffolded project adds db/:
db
├── migrate
│ └── 20261111210824_baseline.sql
└── schema.sqldb/migrate/ is the migration history; db/schema.sql is the generated snapshot the test harness loads. Both are covered in Migrations.
Where the app sits#
Snails works out which app you mean from where you stand. It recognizes two layouts: the one snails new creates, where a single folder is the whole app, and the one where the app sits in src/ inside a repository.
One folder, one app#
snails new myapp gives you a folder that is a whole app. Its database settings and its pins.toml sit side by side:
tree -L 3 --prune -P 'pins.toml|database.rb' myappmyapp
├── config
│ └── database.rb
└── pins.toml
2 directories, 2 filesStart anywhere inside and Snails walks up until it finds the app:
cd myapp/config
snails migrate status[snails] app root /tmp/manual2/myapp (auto-resolved)
[snails] 0 applied, 0 pendingThe first line says where it landed. You don't get one when you are already standing in the app folder.
The app in src/#
A repository can keep its app one level down, in src/, so the repository root stays free for deploy files, documentation, and CI. snails new repo/src gives you that layout:
tree -L 3 --prune -P 'pins.toml|database.rb' reporepo
└── src
├── config
│ └── database.rb
└── pins.toml
3 directories, 2 filesThe app itself is unchanged, just one folder deeper. Start inside it and Snails resolves it the same way.
From the repository root, Snails does not look down. Finding an app below would mean choosing one when there could be several:
[snails] no app root at or above /tmp/manual2/repo (looked for config/database.rb) — pass --app DIRName the folder you are standing in and Snails finds the app inside it:
snails migrate status --app .More than one app#
A folder can hold several apps. Each one is complete and keeps its own pins.toml next to its own config/. The file carries the app's name too, and Snails uses that name for the development database and container:
apps
├── billing
│ ├── config
│ │ └── database.rb
│ └── pins.toml
└── reports
├── config
│ └── database.rb
└── pins.tomlStand inside either and it works as before. From the folder that holds both, name the one you want:
snails migrate status --app billingCommands that take a folder rather than --app work the same way: snails sync billing, run from apps/.
Back to the loop in Getting Started.