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:

  1. 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.
  2. 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.
  3. 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.
  4. Changes to main or util/. 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 User model 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:

  1. Explicit routes — admin, auth, staff console, SEO endpoints, and your apps' includes
  2. Wiki suffix routes — any path ending wiki/create/, create/, or edit/
  3. 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