About SmallStack
A Django platform to build applications on. The admin shell, auth, CRUD pages, REST, MCP, search, webhooks, feeds, a scheduler, approvals, notifications and publishing are already built and themed, so your project starts at the business logic. Production-ready with SQLite or Postgres, Docker, and zero-downtime Kamal deployment.
What's included
Open fullscreen →Philosophy¶
A platform to build apps on, not a starter to grow out of.
- Use what Django gives you — before adding a package, check if Django already has it
- One declaration, every surface — a model + CRUDView becomes HTML, REST, MCP, search, webhooks and CLI
- The built-in apps use public seams — your app registers through the same hooks they do
- Production-ready defaults — secure settings, typed code, SQLite or Postgres, Docker + Kamal
# apps/tickets/views.py
class TicketCRUDView(CRUDView):
model = Ticket
list_columns = ["id", "title", "status"]
enable_api = True # REST + OpenAPI
enable_mcp = True # Claude tools
enable_search = True # full-text search
enable_webhooks = True # signed events
Tech stack¶
| Component | Choice |
|---|---|
| Framework | Django 6 |
| Frontend | Themed admin shell, htmx interactions, six palettes × light/dark |
| Database | SQLite for dev and production; Postgres supported, same test suite on both |
| Search | SQLite FTS5 or Postgres tsvector + GIN, ranked |
| Tasks | Django Tasks + django-tasks-db worker with a @scheduled recurring scheduler — no Redis or Celery |
| Typing | mypy + django-stubs over every app, gated in make lint |
| Package manager | UV |
| Deployment | Docker Compose or Kamal with Let's Encrypt. Your choice. |
One model, every surface¶
Declare a CRUDView once and flip the flags. The same form, queryset and permission logic is emitted on every surface.
- HTML — themed list / detail / create / update / delete pages
- REST — Bearer auth, OpenAPI 3.0, Swagger UI, ReDoc, typed clients
- MCP —
list_tickets,get_ticket,create_ticket… under OAuth + PKCE - Search — ranked full-text with a Ctrl-K omnibar and an MCP retrieval tool
- Webhooks — signed
created / updated / deletedevents with retries and replay - CLI —
sc ls / get / new / set / rm, same validation and audit as REST
$ sc ls tickets.Ticket --json
$ curl -H "Authorization: Bearer $TOKEN" \
/api/tickets/?status=open
$ # Claude Desktop → tools/list
list_tickets get_ticket create_ticket
update_ticket delete_ticket search_tickets
Theming¶
Light and dark modes with selectable color palettes.
- Dark mode toggle — user preference saved in localStorage
- 6 built-in palettes — Purple (default), Django, Blue, Orange, Gold, High-contrast
- CSS custom properties for colors, spacing, shadows — never a hard-coded hex, so every page works on all 12 combinations
- Built on Django admin's responsive CSS foundation

Authentication¶
Built on Django's battle-tested contrib.auth — no third-party packages.
- Custom User model ready for email login
- Signup control — enable/disable registration with a setting
- Password reset via Django's built-in views and email
- Feature flags to toggle app sections on and off

Profile App¶
Complete user profile management, auto-created on signup.
- Photo & cover image uploads with Pillow
- Bio, location, website and display name
- Color palette preference saved per user
- Extend with your own fields — standard Django model

Search¶
Real ranked search, one index, three access tiers.
- FTS5 on SQLite,
tsvector+ GIN on Postgres — BM25-style ranking, no external service - One flag per model —
enable_search = True, withsearch_visibilityfor tenancy - Ctrl-K omnibar +
/search/page — results respect each viewer's staff / signed-in / anonymous tier search_allMCP tool — the same index for AI agents and RAG over your help docs- SearchBuilder — custom variants when the default ranking isn't the one you want
class TicketCRUDView(CRUDView):
model = Ticket
search_fields = ["subject", "body"]
enable_search = True
search_access = SearchAccess.AUTHENTICATED
@staticmethod
def search_visibility(user, qs):
return qs.filter(team__members=user)
Background tasks & scheduler¶
Django 6's Tasks framework with a database backend, plus a recurring scheduler.
- No Redis or Celery —
django-tasks-db; the worker ismanage.py db_worker @scheduled— cron, interval or once, with a themed/smallstack/scheduler/UI, REST + MCP controls and a per-minute tick- Lost-task reaper + heartbeat — stuck jobs are detected and the status page says so
- Docker & Kamal run the worker automatically; only local dev needs to start it
from apps.scheduler import scheduled
from django.tasks import task
@scheduled(every="5m", name="Publish due posts")
@task()
def publish_scheduled_posts():
return service.publish_due_posts()
# One-shot, from a view — returns instantly
send_welcome_email.enqueue(user_id=user.pk)
Webhooks¶
The seam between your systems — both directions, signed, replayable.
- Outbound —
enable_webhooks = Trueemits signedcreated / updated / deletedevents with retries,Retry-Afterand dead-letter replay - Inbound —
@webhook_handlerreceivers; provider signatures via@webhook_verifier, handshakes via@webhook_challenge - SmallStack ↔ SmallStack pairing —
sc webhook pair, loop-safe, stableevent_iddedupe - Collect external feeds —
register_feed_sourcepulls an RSS/Atom source into a model on a schedule, for the systems that only speak RSS
@webhook_handler("stripe", verifier="stripe")
def on_stripe_event(event):
if event.type == "invoice.paid":
mark_paid(event.data["object"]["id"])
@webhook_transform(Ticket)
def ticket_payload(ticket, event):
return {"id": ticket.pk, "status": ticket.status}
Approvals & notifications¶
Human-in-the-loop for your code — and for AI agents.
@approval_kind+request_approval()— any app or MCP agent files a request- Decision console at
/smallstack/approvals/requests/, or the{% approval_card %}embed in your own page - React your way — per-kind callback, signal, webhook event, or polling
- Notifications —
notify()with a topbar bell, unread badge and inbox; approvals fan out to it and to branded email by default
@approval_kind("refund", label="Issue a refund")
def on_refund_decided(request, decision):
if decision.approved:
issue_refund(request.payload["order_id"])
request_approval(
"refund",
payload={"order_id": order.pk, "amount": 120},
requested_by=agent_user,
)
Runbook, blog, newsletter & feeds¶
Publishing is a platform surface too.
- Runbook — versioned markdown documents with a split editor, slide and step views, templates and search
- Blogs — per-user channels with a private / members / public visibility ladder, a compact writing studio at
/my/blogs/, and a system console under/smallstack/blog/ - Publish a runbook page into a blog, optionally resyncing when the page changes
- Newsletter — double opt-in subscribers, one-click unsubscribe, and a durable batched sender that resumes if the worker dies mid-send
- Readers get
/blog/, a sitemap and an anonymous JSON API - Feeds are a publishing surface — RSS/Atom/JSON per blog and site-wide, built as curated
Feeds over scoped querysets; the sameregister_feed()serves a podcast or changelog, with enclosures viaFeedItem(extra_kwargs=…)
post = service.publish_document(
document, blog, sync="auto"
)
issue = service.create_issue_from_post(post)
service.queue_issue(issue) # Delivery rows are the ledger
Activity Tracking¶
Zero-config request logging with a staff dashboard.
- Middleware-based — captures every request automatically
- Staff dashboard with stat cards and filterable log table
- Live refresh via htmx polling — no WebSockets
- Auto-pruning — configurable retention, background task cleanup

Help System¶
File-based documentation viewer — the system you're looking at right now.
- YAML-driven navigation with sections, icons, and ordering
- Markdown rendering with code blocks, tables, and TOC
- Full-text search with client-side indexing
- Slide viewer for focused presentations and walkthroughs

Docker & Deployment¶
Production-ready containers with zero-downtime deployment.
- Docker Compose with web, worker, and health checks
- Kamal — deploy to any VPS with
kamal deploy - Automatic HTTPS — Kamal provisions TLS via Let's Encrypt
- Typical deploy under 60 seconds including build, push, and health check
$ kamal deploy
Building image...
Pushing to registry...
Starting container...
Container is healthy!
Releasing the deploy lock...
Finished all in 38.4 seconds