Architecture
How Pengin-Pi-3 fits together: how the core is organized and where your own code goes, the RBAC framework, the Slug CMS and wiki, dynamic content types, and the subsystems that run on every request.
For what the platform does, see Info. To get it running, see Install.
One app — and where yours go
Pengin-Pi-3 ships as one Django app, main, alongside two supporting
directories: util/ for shared infrastructure and templates/ for the
layouts everything renders into. There are no feature apps in the box.
pengin-pi-3/
├── main/ the core app
│ ├── auth/ the RBAC framework
│ ├── models/ core models, one app_label
│ ├── views/
│ ├── forms/
│ ├── migrations/ one directory for the core
│ ├── management/ manage.py commands
│ ├── settings.py, urls.py, sitemaps.py, admin.py
├── util/ shared infrastructure, no auth logic
├── templates/ layouts, components, core page templates
├── static/
└── your_app/ yours, if you need one
Inside main, the core is organized into submodules — main/models/slug.py,
main/views/wiki.py, main/auth/permissions.py — rather than split into
slug/, accounts/, and cms/ apps. Every core model shares the main app
label, so there is one migrations directory and no cross-app migration
coordination inside the core.
Building on it
What you do after installing is your decision — extend main directly or
add apps of your own. The recommended order is to reach for the lightest tool
that does the job:
- The slug editor and the wiki. Most pages need no code at all. A standard webpage — marketing pages, documentation, one-off content — usually works entirely inside these. See Slugs and Pages and Wiki Module.
- Dynamic content types. For a structured, admin-editable collection — a catalog, a listing, a feed — define a JSON Schema on a slug instead of writing a model. See Slugs and Pages.
- A new Django app. For application needs the core can't cover — real relational structure, custom workflows, your own views and permission rules. Standard MVT, in its own app, registered like any Django app.
- Changes to
mainorutil/. Only for code that belongs to everyone. See below.
The app mechanism is held in reserve for step 3. If the slug editor, wiki, or dynamic types can do it, they should.
What goes where
Your app — its own features. Models, views, forms, templates, URLs, and the permission rules specific to what it does. This is the default home for anything you write.
main — only what is truly universal: something the core's own
components use and that other apps can subscribe to. The user model, the
RBAC framework, the Slug CMS, history auditing.
util/ — middleware, custom libraries, helper scripts and functions,
and utilities that main and apps both subscribe to. Generic by
definition. Nothing in util/ implements auth or permission logic; that
belongs in main/auth/.
Two tests help. For main: would the core, or an unrelated app, call
this? For util/: could an app with nothing in common with yours use it
unchanged?
If your code passes either test, consider submitting it. Contributions to
main that are subscribable by the core and other apps, and util/ modules
that are broadly useful, are welcome upstream. See Contributing.
What ships
Bones only. No blog, no job board, no ticket system. What every project inherits is the foundation those things would otherwise be rebuilt on each time:
- a custom
Usermodel and the RBAC framework around it - the Slug CMS, its self-generating content editor, and the wiki
- dynamic content types backed by FerretDB
- SEO, analytics capture, mail, file storage, and bot defense
- a hardened Docker, Nginx, and Traefik deployment shape
Tobu Pengin maintains application modules separately, some on withheld branches of its internal GitLab. They are outside the supported surface. See Info.
Request path
Client
↓
Traefik TLS termination, host routing — external, not in compose
↓
Nginx static/media serving, rate limits, blocklist
↓
uWSGI 4 processes, 2 threads
↓
Django HardenedBlocklistMiddleware → ... → PageViewLoggerMiddleware
Traefik is expected to already be running on an external Docker network named
root_proxy. The compose file joins it rather than starting it. If that
network doesn't exist, docker compose up fails — see Deployment.
Nginx serves static and media directly from shared volumes; those requests never reach a Django worker.
URL ordering
main/urls.py resolves in three bands, and the order is load-bearing:
- Explicit routes — admin, auth, staff console, SEO endpoints, and your apps' includes
- Wiki suffix routes — any path ending
wiki/create/,create/, oredit/ - The slug catch-all — everything else
Band 2 matches by suffix across the whole site. An app route ending in
create/ or edit/ registered below it is captured by the wiki and returns
404. Register app includes in band 1. See Migrating from Pengin-Pi-2.
Storage layers
PostgreSQL — and FerretDB on top of it
One Postgres instance, running FerretDB's DocumentDB image
(ghcr.io/ferretdb/postgres-documentdb). A plain postgres:17-alpine will
not work: the image bakes in the init scripts that install the
documentdb_api extension, and without them every document operation fails
with schema "documentdb_api" does not exist.
The extension installs into one database, named by FERRETDB_DB and
defaulting to postgres. The application's database (DB_NAME) is created
alongside it in the same instance and volume by an init script on first boot.
The ferretdb service speaks the MongoDB wire protocol against that same
Postgres, so relational and document data share one instance and one backup.
See Database and Document Store.
Redis
The Django cache backend via django_redis, the analytics buffer, request
tracing, and rate limiting.
The Slug CMS
Every page is a Slug (main/models/slug.py). A catch-all SlugView at the
bottom of urlpatterns resolves every unmatched path
Pages Here
No sub-pages yet.
Page Info
Wiki: Docs
Last edited on Sep 21, 2026 by Stuart Anderson
Maintainers
| Editor | Last Activity |
|---|---|
| Stuart Anderson creator | Sep 21, 2026 |