cdn.si

rendered markdown titpetric

User stories

Every story names its routes and the test that proves it. Proof is a venom testcase (tests/venom.yml), a playwright spec (tests/playwright/specs/), a Go test package, or untested. The persona journeys under tests/playwright/specs/journey-*.spec.ts walk a visitor, reader, writer and owner end to end in a browser, overlapping the venom rows below; a story cites the most specific proof it has. untested names a capability no automated test exercises yet.

The cast

visitor - no account. Reads what is public: the firehose at /, public projects and their published articles, tag hubs, search, the Atom feeds and the site documents. Registration and the sign-in doors are the only verbs.

reader - a registered account, no membership. The audience verbs separate them from the visitor: follow, subscribe, like, bookmark, a /feed of their own, notifications, /settings and the export. Reading stays free.

member - a reader who paid the one-time fee (user.member_since). Writing opens: projects, articles and reposts, comments, uploads. Every role below requires it.

project owner - the member who created a project, or accepted an invitation as owner. Runs it: settings, visibility, invitations, the comment queue, the trash, every article in it.

co-writer - a member holding the member role in someone else’s project. An author there: writes, edits drafts and reviews, publishes their own work. Settings and the comment queue stay with the owners.

reviewer - the reviewer role: reads articles in the review and scheduled states before publication and posts review-flagged feedback in the thread. Writes no articles.

subscriber / follower - a reader attached to a source: following a user or a tag, subscribed to a project. An audience relation: it feeds /feed and the notifications, and grants no pre-publish access.

operator - whoever runs the blog CLI on the host. Grants and revokes memberships, audits and restores accounts, suspends, purges articles, restores projects, snapshots and imports. Every verb is a command.

The stories

Visitor

Story Routes Proof
A visitor can read the public firehose GET / venom: landing page renders for an anonymous visitor
A visitor can read a public project’s page GET /u/{username}/{project} venom: /search finds published articles and misses drafts and private ones
A visitor gets a 404 from a private project, page and feed alike GET /u/{username}/{project}, GET /p/{project}/feed.xml venom: a private journal does not exist for strangers
A visitor can read a published article in a project GET /u/{username}/{project}/{article} venom: a draft with markdown publishes and renders
A visitor can read a project-less article under its author GET /u/{username}/{slug} venom: a project-less article has its own drafts, trash and fanout
A visitor holds one address per article GET /u/{username}/{project}/{article} venom: an article moves to another project and takes its url with it; a project-less article has its own drafts, trash and fanout
A visitor can follow a shortlink to what they may read, and get a 404 otherwise GET /_/{id} venom: the shortlink decides for its reader
A visitor sees a scheduled article as a countdown teaser GET /u/{username}/{project}/{article} venom: a scheduled article shares as a countdown teaser
A visitor can read a user’s publishing page GET /u/{username} venom: a javascript link in a bio renders as text
A visitor can browse a tag hub GET /t/{tag}, GET /u/{username}/t/{tag} venom: the tag hub lists the article
A visitor can search published articles, site-wide or in one project GET /search venom: /search finds published articles and misses drafts and private ones
A visitor can read the site documents GET /privacy, /terms-of-use, /contact, /security venom: the site documents render with chrome and footer links
A visitor can subscribe to a user’s or a public project’s Atom feed GET /u/{username}/feed.xml, GET /p/{project}/feed.xml venom: the blog and user feeds answer valid atom
A visitor can read a public reading list GET /u/{username}/bookmarks untested
A visitor can read a public reading list’s Atom twin GET /u/{username}/bookmarks.xml venom: the bookmark feed honours its three visibility settings
A visitor can register, 18 or over, agreeing to the terms GET /register, POST /register venom: registration signs the browser in; registration refuses anyone under 18 and keeps nothing
A visitor can sign in with username or email and password GET /login, POST /login venom: login refuses a wrong password without naming the account; the password changes in /settings and the old one stops signing in
A visitor can request a sign-in link and get one answer whatever the address POST /login/link, GET /login/link/{token} venom: a sign-in link answers one screen for every address

Reader

Story Routes Proof
A reader lands on their feed, which says what fills it when empty GET /feed venom: an empty dashboard says what fills it and where to go
A reader can read their feed as Atom, theirs alone by session GET /feed.xml venom: /feed.xml is the session holder’s own feed
A reader can follow a user POST /u/{username}/follow venom: a second account follows, subscribes, and gets one notification
A reader can unfollow a user POST /u/{username}/unfollow venom: the audience edges are reviewable at /settings/following
A reader can subscribe to a project POST /p/{project}/subscribe venom: a second account follows, subscribes, and gets one notification
A reader can unsubscribe from a project POST /p/{project}/unsubscribe untested
A reader can follow a tag, which lists public articles only POST /t/{tag}/follow venom: a followed tag lists public articles in the feed, and no others
A reader can unfollow a tag POST /t/{tag}/unfollow untested
A reader can review and revoke every edge in one place GET /settings/following venom: the audience edges are reviewable at /settings/following
A reader can like what they can read, idempotently, and nothing they cannot POST /articles/{id}/like venom: likes and bookmarks count, and refuse what the reader cannot read
A reader can unlike POST /articles/{id}/unlike untested
A reader can bookmark what they can read POST /articles/{id}/bookmark venom: likes and bookmarks count, and refuse what the reader cannot read
A reader can unbookmark POST /articles/{id}/unbookmark untested
A reader’s reading list sits behind its visibility setting GET /u/{username}/bookmarks.xml venom: the bookmark feed honours its three visibility settings
A reader can see what reached them GET /notifications venom: a second account follows, subscribes, and gets one notification
A reader can mark their notifications read POST /notifications/read untested
A reader sees their initials disc and the unread bubble in the chrome GET /feed venom: the account disc shows the initials and the unread bubble; playwright: chrome.spec.ts “the account menu opens as a popover and names the account”
A reader can edit their display name and bio, with hostile input rendered as text GET /settings, POST /settings venom: a javascript link in a bio renders as text; the settings refusals answer 422, name the field and keep the typing
A reader can change their password behind the current one POST /settings venom: the password changes in /settings and the old one stops signing in
A reader can change their email behind the password, audited POST /settings venom: the audit log records an email change and the operator can restore it
A reader can change their username; the old address 404s, the shortlink holds POST /settings venom: a rename in /settings moves the page and the old address 404s
A reader can sign out every other session POST /settings venom: sign out everywhere keeps the acting session only
A reader can delete their account behind the password POST /settings venom: self-serve deletion closes the account
A reader can verify their address from the mailed link and resend the nag GET /verify/{token}, POST /verify/resend venom: a reader without the membership reads for free and pays to write
A reader can sign out POST /logout venom: signing out expires the session
A reader can read the pricing and join a waitlist until payments open GET /pricing, POST /pricing/waitlist venom: a reader without the membership reads for free and pays to write; payments render their amounts and stay closed until stripe exists
A reader can open their money page; checkout opens once the deployment sets Stripe GET /settings/payments, POST /settings/payments venom: payments render their amounts and stay closed until stripe exists
A reader wears the badge their paid total earns GET /settings/payments, GET /u/{username} venom: a recorded payment grants and the badge appears
A reader can export their projects and articles any time GET /export.zip venom: the export is the signed-in account’s own
A reader reads for free and lands on /pricing on any attempt to write the writing routes answer 402 venom: a reader without the membership reads for free and pays to write

Member

Story Routes Proof
A member can create a project GET /new, POST /new venom: creating a public blog lands on it; project creation refuses a too-short slug and accepts a route name
A member can create a project over XHR with field-level refusals POST /api/project venom: the api creates a project and refuses a short slug with a field
A member can probe a slug before submitting GET /api/slug-check venom: the slug probe answers taken, short and available, signed in only; playwright: chrome.spec.ts “the new-project form probes slug availability live”
The slug fills in from the title until the member edits it by hand GET /new playwright: chrome.spec.ts “the slug fills in from the title until edited by hand”
A member can open the editor, with or without a project GET /articles/edit venom: the write route opens the editor without naming a project
A member can save a draft POST /articles/edit venom: a draft with markdown publishes and renders
A member can keep editing by id, surviving any rename GET /articles/edit/{id}, POST /articles/edit/{id} venom: an article moves to another project and takes its url with it
A member can save over XHR, and a draft exists from the first paste POST /api/article venom: the api creates a draft for the first paste to land in; a subset save keeps every absent field; the api saves an article and answers the listing redirect
A member sees a citation as the card the published page prints, while writing POST /api/embed venom: a citation answers the card the published page prints; playwright: editor.spec.ts “a citation draws the card the published page prints”
A member writes markdown on one surface, styled where it stands GET /articles/edit/{id} playwright: editor.spec.ts “the toolbar mounts over the body field and bold wraps the document”; “a heading typed as ### reads as a heading”
A member hides the markers and goes on editing what they marked GET /articles/edit/{id} playwright: editor.spec.ts “the pilcrow hides the markers and leaves the document whole”
A table in a member’s body draws as a table, edited cell by cell GET /articles/edit/{id} playwright: editor.spec.ts “a table in the document draws as a table and a cell writes back”
A member can paste an image or a script-free SVG, and upload refuses a scripted one POST /api/upload venom: an image pastes into the draft and uploads; an svg diagram uploads and the upload refuses a scripted svg; playwright: editor.spec.ts “a pasted image uploads and replaces its placeholder”; “the editor refuses a file this blog does not store”
A member lays a picture out left, right or centred, with no raw HTML POST /api/upload, GET /u/{username}/{project}/{slug} go: markdown TestRenderImageAlignment and storage TestReferencedUploadPaths keep the anchor; playwright: editor.spec.ts “a picture laid out with the left anchor floats, and the text wraps beside it”
A member can drop an image from a draft POST /articles/{id}/images/{imageID}/delete venom: an image pastes into the draft and uploads
A member can publish now or schedule for later POST /articles/{id}/publish venom: a draft with markdown publishes and renders; a scheduled article shares as a countdown teaser
A member can unpublish POST /articles/{id}/unpublish untested
A member can send an article to review POST /articles/{id}/review venom: a reviewer reads an article in review before anyone else
A member can trash, restore and purge their article, losslessly until the purge POST /articles/{id}/delete, /articles/{id}/restore, /articles/{id}/purge venom: a deleted article lands in the trash, restores, and purges
A member can work their drafts queues GET /drafts, GET /p/{project}/drafts venom: a project-less article has its own drafts, trash and fanout; a deleted article lands in the trash, restores, and purges
A member can empty their project-less trash GET /trash, POST /trash/empty venom: the project-less trash empties like a project’s
A member can publish under no project, and their followers hear POST /articles/edit, POST /articles/{id}/publish venom: a project-less article has its own drafts, trash and fanout
A member can move an article between projects, and the url follows POST /articles/edit/{id} venom: an article moves to another project and takes its url with it; a refused move commits nothing
A member can cross-list into projects they can write in, and nowhere else POST /api/article venom: the api saves an article and answers the listing redirect; a refused cross-listing leaves no half-made draft
A member can repost by embed, and the card is re-decided per reader POST /articles/edit venom: alice reposts her own article and a stranger reads the card; a repost of a private article is a placeholder for everyone else; a reading list renders one card per marker, in body order
A member can comment where the policy admits them POST /articles/{id}/comment venom: a comment needs the project’s audience, and a subscription grants it; an article closes its thread inside a project that allows comments
A member can edit their own comment, held or published POST /articles/{id}/comment/{commentID}/edit venom: an approve project holds a comment until its owner lets it through
A member can delete their own comment POST /articles/{id}/comment/{commentID}/delete untested
A member sets the comment floor for their project-less articles in /settings POST /settings venom: the comment defaults in /settings govern the project-less thread

Project owner

Story Routes Proof
An owner can configure the project, one setting at a time GET /p/{project}/settings, POST /p/{project}/settings venom: the retention window saves on its own, leaving the other settings
An owner can set visibility, and a private project vanishes for strangers POST /p/{project}/settings venom: a private journal does not exist for strangers
An owner sets the repost floor an article may lower, never raise POST /p/{project}/settings venom: the repost setting is a floor the project sets and an article lowers
An owner sets the comment floors an article may tighten POST /p/{project}/settings venom: an article closes its thread inside a project that allows comments
An owner can invite by mail with a role, and revoke an outstanding invitation POST /p/{project}/settings venom: an invitation binds to its address and its project
An owner can moderate the project’s held comments GET /p/{project}/comments, POST /p/{project}/comments venom: an approve project holds a comment until its owner lets it through
An owner sees every queue they own in one place GET /comments venom: an approve project holds a comment until its owner lets it through
An owner can act on a held comment from the combined queue POST /comments untested
An owner can restore from the project trash GET /p/{project}/trash, POST /articles/{id}/restore venom: a deleted article lands in the trash, restores, and purges
An owner can empty the project trash POST /p/{project}/trash/empty untested
An owner can read an article’s audit trail GET /p/{project}/{article}/audit, GET /articles/{id}/audit venom: a project-less article has its own drafts, trash and fanout (the id route)
An owner can delete the project; readers get 404 and the operator restores POST /p/{project}/settings venom: a deleted project comes back through the operator’s restore

Co-writer

Story Routes Proof
A co-writer can redeem an invitation mailed to their address, and no other GET /invite/{token} venom: an invitation binds to its address and its project
A co-writer can write, edit and publish their own work in the shared project GET/POST /articles/edit, POST /articles/{id}/publish untested

Reviewer

Story Routes Proof
A reviewer hears when an article enters review, again on re-entry GET /notifications venom: a reviewer reads an article in review before anyone else
A reviewer can read a review article before publication GET /u/{username}/{project}/{article} venom: a reviewer reads an article in review before anyone else
A reviewer can post feedback as a review-flagged comment while the state holds POST /articles/{id}/comment venom: a reviewer reads an article in review before anyone else

Subscriber / follower

Story Routes Proof
A follower gets one notification when a followed author publishes, across every listed project GET /notifications venom: a second account follows, subscribes, and gets one notification
A follower hears when an author they follow publishes a project-less article GET /feed, GET /notifications venom: a project-less article has its own drafts, trash and fanout
A follower’s followed tags list public articles in /feed, and no others GET /feed venom: a followed tag lists public articles in the feed, and no others
A subscriber counts as the audience where a comment policy says followers POST /articles/{id}/comment venom: a comment needs the project’s audience, and a subscription grants it
A follower can read a followers-only reading list GET /u/{username}/bookmarks.xml venom: the bookmark feed honours its three visibility settings

Operator

Story Routes / commands Proof
The operator can grant a membership by hand blog member grant venom: registration signs the browser in
The operator can read the waitlists, with verification state blog member waitlist venom: a reader without the membership reads for free and pays to write
The operator can revoke a membership as a sanction a waitlist click cannot undo blog member revoke go: cmd/blog/member
The operator can read an account’s full audit trail blog user audit venom: a rename in /settings moves the page and the old address 404s
The operator can read the email trail and restore a verifiably held address blog user emails, blog user restore-email venom: the audit log records an email change and the operator can restore it
The operator can suspend and unsuspend, and the sign-in stays generic blog user suspend, blog user unsuspend venom: the operator suspends an account and the door stays generic
The operator can purge one article outright, live or trashed blog article purge go: cmd/blog/article
The operator can restore a deleted project, its articles waiting in its trash blog project restore venom: a deleted project comes back through the operator’s restore
The operator can record money taken outside Stripe and list an account’s history blog payment record, blog payment list venom: a recorded payment grants and the badge appears
The operator can snapshot and restore the deployment blog backup, blog restore go: cmd/blog/backup, cmd/blog/restore
The operator can load an export back in as fresh rows under one account blog import go: cmd/blog/importer
The operator lets Stripe settle checkouts, renewals and cancellations POST /stripe/webhook venom: payments render their amounts and stay closed until stripe exists; go: service/payments

Unclaimed routes: none - every @route in frontend/modules/ and every Go route (/feed.xml, /u/{username}/feed.xml, /u/{username}/bookmarks.xml, /p/{project}/feed.xml, /export.zip, POST /stripe/webhook) has a story above that claims it.

Where the data lives and surfaces

Table One row is Surfaces
article one article: metadata, state, counters /u/{username}/{project}/{article}, /u/{username}/{slug}, every listing and Atom feed, /export.zip
article_bookmark one reader bookmarked one article /u/{username}/bookmarks and its .xml twin; the bookmark count on the article page
article_content the markdown of one article the article page, the editor, /export.zip
article_embed one article cited in another’s body the preview card rendered where the marker sits
article_image one image referenced by one article the editor’s image list and its delete button; the reference count the orphan sweep reads
article_like one reader liked one article the like count on the article page; the /feed likes floor
article_project one article listed in one project, home included the project page, /search?project=, the projects: front matter in /export.zip
article_revision one full snapshot of an article at one save /p/{project}/{article}/audit for a project article, /articles/{id}/audit for a project-less one; the controller only - a project owner, or the project-less article’s author
article_tag one tag on one article the article page’s tag line, /t/{tag}, the tags: front matter in /export.zip
comment one comment: state, review flag the article thread, /p/{project}/comments, /comments
comment_body the text of one comment the same thread and queues
email_verification one mailed verification link, token hashed internal, ephemeral by design; consumed by GET /verify/{token}
follow one reader follows one user /settings/following; the follower count on the profile
image one uploaded file /upload/{path} wherever an article references it; uploads/ in /export.zip
login_token one mailed sign-in link, token hashed internal, ephemeral by design; consumed by GET /login/link/{token}
membership_waitlist one account on a plan list CLI: blog member waitlist; the /pricing button state
notification one event addressed to one recipient /notifications; the unread bubble on the account disc
payment one charge: pending, paid or failed /settings/payments; CLI: blog payment list
project one project /u/{owner}/{slug}, /u/{username}, /export.zip
project_invitation one outstanding invitation, token hashed the owner’s list on /p/{project}/settings; redeemed at /invite/{token}
project_member one person’s role on one project the members list on /p/{project}/settings
project_setting one project’s configuration /p/{project}/settings
project_subscription one reader subscribes to one project /settings/following; the audience checks on comments and feeds
tag one word in the site-wide vocabulary /t/{tag}
tag_follow one reader follows one tag /settings/following; /feed
user one account /u/{username}, /settings
user_audit one account field change, append-only CLI only, with no page: blog user audit, blog user emails
user_session one browser session, or an anonymous one holding a CSRF token internal, ephemeral by design; cleared by sign-out everywhere, suspension and deletion

mig creates and owns migrations; it holds no domain data.