Skip to content

Creating a Project

Start a Sillo application from the official starter (with sillo-start or a plain git clone) and understand what you get and why it is shaped that way.

Sillo does not generate applications. It publishes one.

sillohq/starter is a working application (session authentication against a real user model, migrations, a JSON API, a queue) that you copy and make yours. Its CI boots it and calls every route on every push, on three Python versions.

That distinction is the whole design, and the rest of this page explains why it is worth caring about.

sillo-start is a separate tool, installed once and used for every project after that.

Terminal window
uv tool install sillo-start

Installs it as a standalone command on your PATH, in its own isolated environment. Nothing is added to any project.

Python 3.11 or newer. Check it is there:

Terminal window
sillo-start --help
Terminal window
sillo-start create-app myapp
cd myapp

Then install dependencies and set the database up:

Terminal window
uv sync
uv run sillo db:migrate
uv run uvicorn app:app --reload

Then open http://localhost:8000.

sillo-start fetches the starter, renames the project to yours, and writes a .env with freshly generated secrets. It prefers uv when it is installed and falls back to pip when it is not, so neither path needs anything the other does.

Everything after that is the sillo command, which the project has as soon as dependencies are installed. sillo on its own lists what this project can do.

The starter ships a uv.lock, so uv sync reproduces the exact set of versions its CI tested. Poetry and PDM install it too (the pyproject.toml is standard PEP 621) but they resolve fresh rather than reading that lock.

create-app takes the repository as an argument. Any public GitHub repository works, so your own team template is a first-class option.

Terminal window
sillo-start create-app myapp # sillohq/starter
sillo-start create-app sillohq/starter-inertia myapp # the React one
sillo-start create-app sillohq/starter@v1.2 myapp # pinned to a tag
sillo-start create-app acme/our-template myapp # your own
Option
--ref <branch|tag>Which revision to take. Defaults to main
-d, --directory <path>Where to create it. Defaults to ./<name>
--installInstall dependencies straight away
--no-gitDo not initialise a git repository
--forceAllow a directory that is not empty
-v, --verboseShow tracebacks

Dependencies are not installed by default, so creating a project takes a second rather than a minute. --install does it during creation if you would rather not run the install step yourself.

The starter is a real repository, so cloning works and gives you the same application. The one difference is that .env is not written for you:

Terminal window
git clone https://github.com/sillohq/starter.git myapp
cd myapp
cp .env.example .env # then set SECRET_KEY to something of your own

Then install and migrate as above.

Prefer this when you want to keep pulling from upstream. Prefer create-app when you want the project named after itself from the first commit, with no upstream history to delete.

  • Python 3.11 or newer

uv is recommended and is what these guides use, but nothing here requires it. venv and pip ship with Python and do the same job.

SQLite needs nothing else. PostgreSQL or MySQL need a running server and one extra driver. See Database & Migrations.

A generator renders templates. Templates get checked for rendering, which is not the same as working.

A generated project can produce valid Python, import cleanly, render every page, and still fail on its first real request. All of these render perfectly:

  • middleware registered in an order that puts authentication outside the session it reads from
  • an auth backend reading the id claim from a token that carries sub
  • a /static mount that was never added, so every stylesheet 404s in production
  • a queue that accepts jobs and never runs them

Every one of those was a real bug in this project’s history, and none of them is visible in a rendered template. They surface when something calls the application.

So the starter is an application, and its CI runs it:

- name: Apply migrations
run: |
cp .env.example .env
uv run sillo db:migrate
- name: Test
run: uv run pytest -q
- name: Create an administrator
env:
ADMIN_PASSWORD: Ci-password1!
run: uv run sillo user:admin ci@example.com ci
- name: Boot the application
run: uv run python scripts/smoke.py

The last step boots the app and calls every route. What you clone has been run, not merely written.

It has a second consequence worth knowing: a bug in what you get can be fixed without releasing a tool. The starter is a repository; a fix is a commit. Nothing has to be published to PyPI and nothing on your machine has to be upgraded before the next person who clones it gets the fix.

AuthSession-based over JSON, with JWT written and commented out
UsersOne User model with a manager, password hashing and verify_credentials
DatabaseRecord with SQLite by default, and real migrations
PagesOne HTML page and a stylesheet, with /static served in development
APIJSON routes under /api, documented by Atlas at /docs
QueueA worker and scheduler, wired and switched off
Consolesillo: migrations, users, worker, scheduler, serve
Toolingruff, pytest, a smoke check, and CI on three Python versions

Two things are deliberately not there, and both are covered later:

  • No background jobs. app/jobs/ is an empty package. What a job should do is your application’s business, and an example you have to delete is worse than none. See Background Work.
  • No second user model. One User, and everything authenticates against it. See Users & Authentication.

Whichever route you take, the same four things happen. Worth knowing because each one is a decision you may want to change.

create-app downloads a tarball from codeload.github.com/<owner>/<repo>/tar.gz/<ref>, rather than cloning. That needs no git on the machine, brings no history for you to delete before your first commit, and pins to a tag as easily as to a branch.

GitHub wraps the archive in a single top-level directory named after the repository and commit; that prefix is stripped so the project’s files land at your directory’s root.

An archive member whose path escapes the destination is refused rather than sanitised. That is how a malicious archive overwrites files elsewhere on your machine.

Rewriting is targeted, not a blanket find-and-replace, so prose that happens to say “starter” (a README sentence, a comment) is left as written. What changes:

FileWhat
pyproject.tomlname = "myapp"
app/config.pyapp_name, the module docstring, the SQLite path
.env.exampleAPP_NAME, the SQLite path, the header comment
uv.lockThe project’s own name, so uv sync still resolves

Model files are deliberately excluded. A model’s docstring becomes its table_description in the database, so rewriting one puts your models out of step with the committed migration, and the next sillo db:make writes a spurious second one describing nothing but a changed comment.

.env is created from .env.example with a fresh value for SECRET_KEY, JWT_SECRET and APP_KEY.

A secret committed to a starter is a placeholder by definition. Without this step every project created from it would sign its sessions with a key published on GitHub. Cloning directly skips this step, which is why .env.example has to be copied and edited by hand.

An existing .env is never touched. It may hold real credentials.

git init, unless you pass --no-git. No initial commit is made; the first commit is yours.

sillo-start has one command. That is a design decision, not an omission.

A tool that also manages projects (generating models, editing config, running migrations, supervising processes) has to keep working against every version of every project it ever generated. A tool that only creates them is finished the moment the files land.

So everything a project needs after it exists comes from the framework’s sillo command, which reads it off the application:

Terminal window
sillo db:migrate
sillo user:admin ada@example.com ada
sillo queue:work
uvicorn app:app --reload

sillo finds the application and derives those from what it set up. The operations underneath stay plain functions (sillo.record.commands, sillo.users.commands, sillo.work.commands) so a project that wants different names can build its own console against them. See The Console.

Nothing in a created project depends on sillo-start. You can delete it the moment your project exists, and a change to it can never break what it already made.

Any public GitHub repository works:

Terminal window
sillo-start create-app acme/our-template myapp

For a company template (your own base models, your own middleware stack, your own deployment files) fork the starter, change what you want, and point people at yours. The rename rules look for these files, and skip any that are absent:

RENAMES = (
("pyproject.toml", ('name = "{old}"', 'name = "{new}"')),
("app/config.py", ('"""Typed settings for {Old}."""', '"""Typed settings for {New}."""')),
("app/config.py", ('app_name: str = "{Old}"', 'app_name: str = "{New}"')),
("app/config.py", ("sqlite://storage/{old}.db", "sqlite://storage/{new}.db")),
(".env.example", ("# {Old} environment.", "# {New} environment.")),
(".env.example", ("APP_NAME={Old}", "APP_NAME={New}")),
(".env.example", ("sqlite://storage/{old}.db", "sqlite://storage/{new}.db")),
)

{old}/{new} are the lowercase names, {Old}/{New} the title-cased ones. Keep those lines recognisable in your fork and renaming keeps working; change them and the project simply keeps the template’s name, which you can fix by hand.

Terminal window
sillo db:migrate # create the database
sillo user:admin you@example.com you # a superuser account
uvicorn app:app --reload # http://localhost:8000

sillo on its own lists every command this project has.

Then:

app/bootstrap.py is the one file worth reading first. Everything the application is made of is assembled there, in order, with the reasoning written down beside each step.

Collected from actually running this, not from reading the source.

  1. Make sure you are running the project’s own sillo. A virtual environment activated in a parent directory shadows the project’s, and the sillo it finds there is usually older than the project needs. uv run sillo settles it under uv; under venv, activate the project’s own .venv first.

  2. Installing dependencies is not the same as creating the database. Installing gets you the sillo command; sillo db:migrate is what creates the schema.

  3. The database file is gitignored, the migrations are not. Commit database/migrations/; it is the schema’s source of truth.