Architecture
Differences from upstream
Upstream Misskey runs everything in one kind of Node.js process, with Redis and PostgreSQL. misskey-cf splits it into Workers by role and replaces Redis and the job queue with Cloudflare services.
| Component | Upstream | misskey-cf |
|---|---|---|
| Runtime | Node.js process | 5 Workers (split by role) |
| Database | PostgreSQL | PostgreSQL (same schema, via Hyperdrive) |
| Redis | Caches, timelines, notifications, pub/sub | Not used (replaced by Durable Objects, KV, and PostgreSQL) |
| Job queue | BullMQ | Cloudflare Queues |
| Periodic tasks | BullMQ repeatable jobs | Cron Triggers |
| File storage | Local / S3 | R2 |
| Image and video processing | sharp / ffmpeg (same process) | Containers (sharp + ffmpeg) |
| SMTP | Cloudflare Email Service | |
| Full-text search | Meilisearch and others (optional) | Not used (SQL substring match) |
| Web client | Upstream's Vue client | Upstream's Vue client, built and served as-is |
Workers
Only the gateway is public. The other Workers are called only through service bindings, Durable Objects, and Queues.
| Worker | Role |
|---|---|
| gateway | The only public Worker. Serves the web client, generates HTML, and forwards everything else to api |
| api | Every API endpoint, ActivityPub in and out, drive, mail, Web Push, job processing, periodic tasks |
| timeline | Delivers posts to followers' timelines and to streaming. Stores notifications |
| stream | Per-user WebSocket connections and online presence |
| media | Thumbnails and other derivatives, media proxy (sharp / ffmpeg in a Container) |
What replaces Redis and the job queue
| Upstream mechanism | misskey-cf |
|---|---|
| Caches for meta, roles, and so on | Per-execution-environment memory cache and KV |
| Cache invalidation across processes | None (caches expire) |
| Streaming event delivery | Sent directly to a per-user Durable Object |
| Home, list, and channel timelines | PostgreSQL queries that return the same results |
| Antenna timelines | Durable Object |
| Notification storage | Per-user Durable Object |
| Rate limits | Durable Object |
| Rankings | Aggregated over the same window in PostgreSQL |
| ActivityPub delivery and inbox | Queues |
| Webhook delivery | Queues |
| Account deletion, exports, imports | Queues (one step per message) |
| Poll endings, scheduled notes | Durable Object alarms |
| Relationship processing (follows and so on) | Handled within the request (only ActivityPub delivery is asynchronous) |
| Chart recording | None (computed from the source data on request) |
Main flows
Posting
The post is saved within the API request, and delivery to followers' timelines happens asynchronously through a Queue.
When there are remote followers, api also enqueues ActivityPub delivery on a separate Queue.
Streaming
A WebSocket connection goes from the gateway to api, which authenticates it and hands it to the user's Durable Object (stream). From then on, api and timeline send events to that Durable Object, which forwards them to the socket. While connected, the user's last-active time is refreshed periodically.
ActivityPub
Incoming activities from remote servers are enqueued and acknowledged at once; signature verification and processing happen in the Queue consumer. Outgoing deliveries are also enqueued per destination, and failed deliveries are retried with backoff.
Cloudflare services in use
| Service | Use |
|---|---|
| Workers | The 5 Workers |
| Static Assets | Upstream's web client |
| Durable Objects | Timelines, notifications, WebSockets, online presence, rate limits, timers |
| Queues | ActivityPub in and out, timeline delivery, webhooks, background jobs |
| Hyperdrive | Connection to PostgreSQL |
| R2 | Drive files |
| KV | Caches for meta and the emoji list |
| Containers | Image and video processing (sharp + ffmpeg) |
| Email Service | Mail delivery |
| Cron Triggers | Hourly periodic tasks |