starcms
A code-first CMS for Python. Define your content models as Pydantic classes and get a login-protected admin UI plus a content API, mounted into your existing FastAPI or FastHTML app — pure Python, no Node, no build step, no separate service.
from fastapi import FastAPI # or: from fasthtml.common import FastHTML
from pydantic import BaseModel
from starcms import StarCMS
class BlogPost(BaseModel):
title: str
body: str | None = None
published: bool = False
app = FastAPI()
cms = StarCMS(db="sqlite+aiosqlite:///content.db", models=[BlogPost])
cms.mount(app, admin="/admin", api="/api/cms")
That's the whole integration. From one model class you get:
-
/admin— a server-rendered admin: list, create, edit, delete, behind a login, with forms generated from your model's fields and validated by Pydantic. -
/api/cms/blogpost— a read-only JSON API serving your content to frontends and mobile apps (opt-in: mounted only if you passapi=). Collections come enveloped as{"items": [...]}and paged (?limit=default 50, max 200, plus?offset=); single records live at/api/cms/blogpost/{id}. -
In-process queries — server-rendered hosts read their own content directly, no HTTP round-trip to yourself. Records come back as plain dicts (
{"id": ..., **fields}):posts = await cms.find(BlogPost, published=True, limit=10) post = await cms.get(BlogPost, 42)
Change the class and the admin forms, table, database schema, and API all follow. The model is the single source of truth.
Why
Python is underserved for modern, code-first content management. Wagtail and Django CMS are mature but Django-coupled; the popular headless CMSes (Payload, Strapi, Sanity) mean running a Node service next to your Python app — two runtimes, two deploys, and your content schema living outside your codebase. Admin generators like SQLAdmin solve the screens but stop there: no content delivery, and your models must be SQLAlchemy ORM classes.
starcms is a library, not a service: pip install, describe content in the
Pydantic vocabulary you already use, mount, done. It works in both FastAPI
and FastHTML because it's built one layer down, on Starlette — the
foundation they share.
Install & run
uv add starcms # or: pip install starcms
Set the admin credentials and (in production) a session secret:
| Variable | Purpose |
|---|---|
STARCMS_ADMIN_USER / STARCMS_ADMIN_PASSWORD |
The single admin login. Unset = admin locked. |
STARCMS_SECRET |
Signs the session cookie. Unset = random per process (dev-only; restarts log you out). Set it before cms.mount(). |
Then run your app as usual (uvicorn myapp:app). The database tables are
created automatically on first use — no migration step. SQLite
(sqlite+aiosqlite:///...) for development, PostgreSQL for production
(postgresql+asyncpg://... — install via pip install "starcms[postgres]").
Supported field types
str, int, float, bool, datetime.datetime — each optionally
| None, with defaults and default_factory respected (a
default_factory=datetime.now prefills forms with now, not server start).
Labels come from field names or Field(title=...). Unsupported types are
rejected loudly at startup, not deep in a request. Models must not declare
an id field — starcms manages the primary key.
The shape of it
your Pydantic models
│ introspection (one pass, cached)
▼
FieldSpec IR ──────────┬───────────────┬──────────────┐
│ │ │ │
▼ ▼ ▼ ▼
database tables admin forms admin tables JSON API
(SQLAlchemy Core) (htpy, no JS) (read-only)
One introspection layer reads your models; everything else is generated from its output. The admin is a self-contained Starlette sub-app with its own session (cookie named and path-scoped so it never collides with your app's). The API is a second sub-app with a separate perimeter: no session, no login — front it with your own auth if your content isn't public.
Honest scope
This is the wedge, deliberately small: CRUD admin + content API. Not yet here: rich text, media uploads, drafts/versioning, multi-user auth & roles, migrations (schema changes during dev: drop and recreate), localization, htmx interactivity. These arrive based on real demand, not speculation.
Development
git clone https://github.com/admsftpge/starcms && cd starcms
uv sync
uv run poe check # lint + tests
uv run poe demo-fastapi # admin at http://localhost:8000/admin (admin/admin)
uv run poe demo-fasthtml # same CMS, FastHTML host, content on the homepage
License
MIT
Metadata
Release files for starcms 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| starcms-0.1.0.tar.gz | 16.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| starcms-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 37.3 kB
Release files / starcms-0.1.0.tar.gz
| Download URL | starcms-0.1.0.tar.gz |
|---|---|
| Size | 16.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
9c71de672631d820a692c92099ae1a8e18f9e15405dfdc338d045a96ec6939ba
|
|
BLAKE2b-256 checksum How to use checksums |
859fae8b7c599494f72356cde69b0d3442738f86665c1a044bbd6679ba5528a2
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.11.19 {"installer":{"name":"uv","version":"0.11.19","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / starcms-0.1.0-py3-none-any.whl
| Download URL | starcms-0.1.0-py3-none-any.whl |
|---|---|
| Size | 20.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
328b3dbf50151c12e4d55e96bec0fcca235d43e13bda5d077c1d1800f01a70f9
|
|
BLAKE2b-256 checksum How to use checksums |
8e73937b2226a1f83b6b325269a9fce0f092db74903b5bc717566d970d5a361b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.11.19 {"installer":{"name":"uv","version":"0.11.19","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|