Platform / publishing

Blogs and newsletters, built in.

A blog is a channel: an owner, a slug, a visibility level. Posts are markdown. Any blog can also be a newsletter with double opt-in and a durable sender. It is all in the framework, themed, with feeds, a public read API and the same REST and MCP surface as everything else — so your project starts at the writing.

Two surfaces, one set of rules.

Authors write in their own studio. Operators manage every blog from the admin console. Both call the same service, so there is one idea of "publish", one slug rule, one rendered body.

The author's studio: their blogs, drafts and published posts
The studio at /my/blogs/. Any signed-in user can own a blog; drafts, scheduling and publishing happen here.
The public blog index
The public site at /blog/. Themed, with per-blog pages, permalinks, tags, an archive and feeds — gated by one setting until you are ready.
1

Visibility is a ladder

Private (owner and staff), members (anyone signed in), public (the internet). A post's own lifecycle sits on top: draft, scheduled, published, archived. Both gates apply.

2

Newsletter when you want it

Turn it on per blog. Subscribers confirm by email (double opt-in), issues are snapshots of what was sent, and the sender resumes if the worker restarts. RFC 8058 one-click unsubscribe included.

3

Runbook pages publish too

A versioned runbook document can be published into a blog and kept in sync — write operational docs once, publish the ones worth sharing.

Publish from your own code.

Every write goes through the service, which keeps slugs unique, renders the body and stamps the timestamps — from the studio, the console, REST, MCP, or a line in your own app.

from apps.blog import service

post = service.create_post(
    blog,
    title="October release notes",
    body_markdown="## What changed\n\n- Calendars…",
    tags=["release"],
    actor=request.user,
)
service.publish_post(post, actor=request.user)   # or at=<datetime> to schedule

Publishing is always an explicit act — nothing goes live because a row was saved.

Reading, for anyone

  • /blog/, /blog/<blog>/, /blog/<blog>/<post>/
  • /blog/<blog>/feed.rss — .atom and .json too
  • /blog/api/blogs/… anonymous JSON read API for your own front end
  • /feed/blog.rss the site-wide river

Writing, for your apps and agents

  • /smallstack/api/blog/posts/ REST, authenticated and owner-scoped
  • MCP: list_posts, create_post, update_post — Claude can draft for you
  • Webhooks on create and update, for Slack or a static-site rebuild
  • Datasets: smallstack_blog_posts, …_issues with correct send-rate ratios

A changelog, a company blog, a letter.

All three are the same app with different settings. Ask a coding agent to set one up and it will reach for the built-in rather than new models.