AnyPub is a local-first CMS for writing and scheduling articles across a user's standard.site publications.
apps/web: Next.js App Router UI using shadcn-style source components.packages/block-editor: publishable@stygian/markdown-editorReact package; AnyPub-specific image storage stays inapps/web.services/backend: Swift/Vapor API with Fluent SQLite persistence.- Drafts, cover assets, OAuth state, publish attempts, and calendar links are stored off-protocol in SQLite and the local filesystem.
Published articles use a canonical site.standard.document record with shared metadata, plaintext fallback, optional coverImage, and a host-native structured content union:
- Leaflet:
pub.leaflet.contentwith linear-document pages and Leaflet block records. - Offprint:
app.offprint.contentplus anapp.offprint.document.articlestrong-reference wrapper. - pckt:
blog.pckt.contentplus ablog.pckt.documentwrapper linked to the existingblog.pckt.publication.
The backend validates the editor's schema-v1 block snapshot, preserves headings, quotes, nested lists, task state, code languages, thematic breaks, and UTF-8-indexed rich-text facets. Legacy Markdown-only drafts use the same canonical parser. Large Leaflet and pckt bodies automatically switch to their lexicon-defined blob modes; Offprint bodies are size-checked before publication.
Publishing a known host without a valid adapter is rejected before any remote write. New Offprint and pckt wrapper failures trigger compensating deletion of the canonical document, and wrapper-backed posts remove both records when reverted or deleted.
Scheduling creates or updates community.lexicon.calendar.event records linked to the article URL and document AT-URI when available.
Published documents are public repository records, so POST /api/drafts/backfill imports every
site.standard.document in the account's PDS that no local draft tracks yet. The workspace runs it
once per account per session and again on a manual publication sync, so posts written outside
AnyPub appear under Published with their document AT-URI, CID, and — for Offprint and pckt — their
host wrapper record, which keeps unpublish and delete complete.
Bodies are read back into Markdown from whichever content union the host wrote (pub.leaflet.content,
app.offprint.content, blog.pckt.content, or at.markpub.markdown), including offloaded blob
bodies, rich-text facets, nested and task lists, code languages, and thematic breaks; a document
whose content cannot be read falls back to its textContent. Cover and body images are downloaded
into local assets that keep the original blob reference, so republishing reuses the published blob
instead of uploading a copy. Images that cannot be downloaded degrade to links, and documents whose
site is not a cached publication are skipped.
AT Protocol accounts are linked through discovery, PAR, PKCE, DPoP-bound token exchange, encrypted token/key persistence, DPoP nonce retry, and refresh-token rotation. Existing accounts created before these fields and scopes were added must reconnect. Production startup requires TOKEN_ENCRYPTION_KEY to be valid base64 containing at least 32 bytes.
bun install
bun run devBackend-only:
cd services/backend
swift run AppFull verification:
bun run verifyGitHub Actions runs path-aware checks for pull requests and pushes to main or dev:
- Changes to
apps/web,packages/block-editor, or the root Bun/Turbo build files run the frontend typecheck, lint, complete Vitest suites, and production build. - Changes to
services/backendrun the complete Swift test suite and a release build. - Shared CI or workflow changes run both pipelines. Documentation-only changes skip both while
still passing the stable
Required CI gatecheck. - Railway development tracks
devdirectly and deploys only the affected service after GitHub CI passes.
Configure GitHub and each Railway service before enabling deployments:
- Protect
mainand require theRequired CI gatestatus check. - Connect both Railway services to this GitHub repository and track the environment's deployment
branch (
devfor development). - Enable Railway's Wait for CI setting on both services.
- Keep the service watch paths in the committed
railway.jsonfiles; patterns are relative to the repository root.
No Railway token or deployment secret is required in GitHub Actions. Railway owns deployment and uses the committed Dockerfile configuration for each service.
The shared development stack runs in Railway's development environment:
- Web:
https://testing.anypub.at - API and OAuth metadata:
https://api.testing.anypub.at - API state: a persistent Railway volume mounted at
/data
The web service uses the root railway.json and
Dockerfile.web. The API service uses
services/backend/railway.json and
services/backend/Dockerfile. Its Railway
root directory is /services/backend; run manual deployments from the
repository root so the config's repository-absolute Dockerfile path resolves
the same way as GitHub deployments.
Required Railway variables are documented in .env.example.
Set APP_ENV=dev on the development Web service to show the environment banner;
production defaults to no banner when the variable is omitted.
TOKEN_ENCRYPTION_KEY must be stored as a Railway secret and must not be
committed. Marque is authoritative for anypub.at; both testing hostnames use
Railway-provided CNAME and _railway-verify TXT records managed there.
The Railway production environment is intentionally unconfigured. Promote or
configure it only after the testing environment has been reviewed.