Real-time collaborative lists and kanban app built on Elixir/Phoenix/Ash: live sync, roles and invitations, optimistic locking, LexoRank ordering, and GDPR compliance.
Listify is a real-time collaborative lists and kanban app: several people edit the same lists, see changes appear instantly, and manage members, roles and notifications. A greenfield project driven by a detailed specification, in production and self-hosted.

I wanted a real testbed to push the Elixir/Phoenix/Ash stack on a non-trivial domain: real-time collaboration. Not a demo to-do list, but a complete app — edit concurrency, roles, invitations, notifications, audit, GDPR — driven by a spec written before the code, to lock the architecture decisions up front rather than improvising them.
The app is 100% Phoenix LiveView — no SPA framework, interactivity is server-driven over WebSocket. Business logic, persistence and authorization all go through Ash 3.4 (AshPostgres), organised into domains: Accounts, Lists, Messaging, Notifications, Audit, Backups, Moderation.
Phoenix LiveView (server-driven UI)
│
├── Ash domains (logic, authorization, persistence)
│ Accounts · Lists · Messaging · Notifications · Audit · Backups
├── Phoenix PubSub (real-time signalling)
├── Oban (jobs: mailer, exports, scheduled, critical)
└── PostgreSQL 16 + Redis (sessions)
Every item or list mutation publishes a message to a PubSub topic (items:list:<id>). But the key point: the LiveView does not consume the broadcast payload — it receives a plain signal ({:item_updated, id}) and re-reads the full resource through Ash, with authorization and policies applied. PubSub is a signalling mechanism, not a data transport — which eliminates the entire class of “stale payload” bugs.
Concurrent item edits are guarded by a lock_version checked atomically in the SQL WHERE clause (not after fetch — so no race). A conflict returns a 409 with the server’s version so the client can re-fetch. But only the “heavy” fields (title, description, labels) are guarded; hot fields (status, rank, assignment) go through separate last-write-wins actions to stay fast. This isn’t pessimistic locking — it’s targeted conflict detection.

Item ordering uses LexoRank (rank strings) rather than integer positions: reordering inserts a rank between two neighbours in O(1), never renumbering or locking a global sequence. Two concurrent reorders don’t coordinate — the bit-string math guarantees monotonicity.
Queue topology fixed in the spec: default (10), mailer (5), exports (3), scheduled (2), critical (1). Around fifteen workers: invitation and mention mailers, due-date reminders, daily digest, recurring-list resets, GDPR export, deferred account purge, S3 backup cleanup.
listify.josephpire.dev (Docker Swarm, 2 replicas, rolling updates)Related work