# Elements vs Ably Ably is a hosted realtime messaging company. Apps publish messages to named channels on Ably's network, and every browser or mobile client subscribed to a channel receives them over a persistent connection. Ably's products, including Pub/Sub and LiveSync for streaming database changes, run on that network ([Ably docs](https://ably.com/docs)). Its revenue is usage: paid plans charge a monthly base fee plus usage, billed either per minute (messages, connection minutes, channel minutes and data transfer) or per monthly active user ([Ably pricing](https://ably.com/pricing)). Elements is not a messaging company. It is an integrated app environment for building web apps, built for people and their agents: one coherent system that includes a project server that runs while you work, a default app framework, a package installer, a test runner, a bundled Postgres database, and deploy. Realtime is part of the framework: [LiveTable](/learn/man/livetable) keeps database rows in sync in every browser showing them, and [channels](/learn/man/channel) send any other message from the server to the browsers listening. Both run inside your app, and Postgres, which your app already uses, carries the messages between servers. Elements charges for that tooling, per machine, and meters nothing ([pricing](/pricing)). The difference is that realtime in Elements is code in your app, so the build checks it along with everything else. When you or your agent change a field, a query or a template, the build reports each break in the realtime code, a stale field, a bad query or a failing test, in milliseconds. With Ably, realtime is a second platform. Your app saves the data, sends the change to Ably's network and issues tokens for Ably's clients, and on the per-minute model you pay for every message, every connection minute and every channel minute. In Elements, one write saves the row and updates every browser watching it, and the page arrives with its data already rendered. ## At a Glance | | Elements | Ably | |---|---|---| | What it is | An integrated app environment with realtime built into its default framework | A hosted realtime messaging platform | | What you pay for | Tooling, per machine, per month ([pricing](/pricing)) | A base fee plus usage, metered per minute or per monthly active user ([pricing](https://ably.com/pricing)) | | What is counted | Nothing | Every publish and every delivery to each subscriber, plus history reads and writes ([billing](https://ably.com/docs/platform/pricing)) | | Where your messages go | Your servers and your Postgres | Through Ably's network | | Keeping browsers in step with the database | One write saves the row and updates every watching browser | Your backend writes each change twice, to your table and to an outbox table, and an Ably connector forwards it | | Joining the first data and later changes | Automatic: the page listens before it queries | A `sync()` function you write, plus a sequence id | | Showing a change at once in the sender's browser | Built into every LiveTable | A separate SDK, with mutation ids you pass through your backend | | Checking the code | The build checks it from server to template | Nothing checks that publisher and subscriber agree | | Stored data | Rows stay in your Postgres tables, with no retention window | Ably keeps messages for two minutes by default; longer retention counts extra messages | ## Realtime Without a Second Platform Realtime is what puts one person's change on everyone else's screen: chat, live dashboards, shared lists, presence. A framework without it sends you to a hosted platform like Ably, with its own account, keys, token endpoint, client library and bill. Your users' messages then pass through someone else's network. In Elements, realtime ships in `@elements/app` and runs inside your app, on your servers. Every app server listens on Postgres with `LISTEN` and `NOTIFY`, so a message sent from one server reaches browsers connected to any of them ([channel](/learn/man/channel)). There is no second platform to sign up for, no API keys or tokens to issue and no client library to install. Your messages never leave your infrastructure. Realtime deploys with the rest of the app, in the same `elements deploy` ([deploy](/learn/man/deploy)). ## Ably Meters Messages and Minutes On the per-minute model, Ably's bill has a base package and four meters: messages, connection minutes, channel minutes and data transfer. Messages are counted every time they move: when one user publishes a message and ten users receive it, Ably counts 11 messages. Connections are billed by the minute they stay open, and channels by the minute they are active. Persisting a message to history counts it again, and reading it back counts once more ([Ably billing](https://ably.com/docs/platform/pricing), [storage](https://ably.com/docs/storage-history/storage)). Each plan also caps concurrent connections and message rates. Past a connection limit, new connections are restricted until others close, and publishes over a rate limit are rejected with an error ([Ably limits](https://ably.com/docs/platform/pricing/limits)). Connection minutes grow with your audience and channel minutes with your open channels, whether or not anything happens: a live page left open all day costs connection minutes even if nothing on it changes. In Elements, a broadcast to ten browsers or ten thousand costs what your servers cost. Messages, connections and bandwidth never appear on an Elements bill ([pricing](/pricing)). ## Ably Syncs Your Database Through an Outbox The data most realtime pages show lives in a database, so the hard part is keeping the screen in step with the rows. Ably's answer is LiveSync. Your backend writes each change to your tables and, in the same transaction, writes a matching event to an outbox table. A trigger on the outbox calls Postgres `NOTIFY`, a database connector hears it through `LISTEN`, and the connector publishes the event to Ably's channels ([Postgres connector](https://ably.com/docs/livesync/postgres)). In the browser, the Models SDK needs two functions from you: `sync()`, which fetches the current state from your backend, and `merge()`, which folds each incoming event into it. For optimistic updates, your frontend generates a mutation id, sends it to your backend, and your backend writes it to the outbox, so the SDK can match the confirmed change to the one it already showed ([Models SDK](https://ably.com/docs/livesync/postgres/models)). That is Postgres `LISTEN` and `NOTIFY`, routed through a connector and a second network, with an outbox, a sync function and a merge function for you to write. Elements uses `LISTEN` and `NOTIFY` directly. Every app server listens on Postgres and forwards each change to the browsers connected to it ([channel](/learn/man/channel)). Changes made outside the app need no outbox either. A Postgres trigger that calls `pg_notify` on a table's channel sends each insert, update and delete straight to the browsers watching it, so a row written by a job, a script or `psql` appears on open pages ([channel](/learn/man/channel), [live from SQL](/learn/man/recipes/live-from-sql)). ## One Write Saves the Row and Tells Every Browser In Elements, a [LiveTable write](/learn/man/livetable/mutations) is one call. It runs your handler, saves the row, and broadcasts it to every browser watching that table or that slice of it. There is no outbox to write and no event to publish. Authorization is ordinary code in the route that opens the page and in the table's own handlers, where the session is already available ([partitions](/learn/man/livetable/partitions), [handlers](/learn/man/livetable/handlers)). Here is a chat room in three files, with its sign-in check: ```ts // app/shared/services/chat.ts import { LiveTable, session } from "@elements/app"; export interface Message { id: string; roomId: string; author: string; body: string; createdAt: Date; } export let messages: LiveTable = new LiveTable({ insert: (item) => { session.isLoggedInOrThrow(); return messages.insert({ ...item, author: session.getOrThrow("userName") }); }, }); ``` ```ts // app/pages/room/index.ts import { Request, Response, session } from "@elements/app"; import { messages } from "#app/shared/services/chat"; import html from "./template"; export default function route(req: Request, res: Response) { session.isLoggedInOrThrow(); return new html({ messages: messages.view({ roomId: req.params.id }) }); } ``` ```ehtml // app/pages/room/template.ehtml import { session, LiveView } from "@elements/app"; import type { Message } from "#app/shared/services/chat"; function send(messages: LiveView, draft: { body: string }) { messages.insert( { body: draft.body, author: session.get("userName")!, createdAt: new Date() }, () => draft.body = "", ); } , private draft: { body: string } = { body: "" })>
  • +a.createdAt - +b.createdAt)}> {m.author} {m.body}
send(messages, draft)}>
``` The route checks the session and opens the room's messages. The handler takes the author's name from the session, so no one can post as someone else. Every browser in the room sees a new message as soon as it is saved, and a browser in another room never receives it, because each room is its own partition with its own broadcast. A view opened by a signed-in user also stops when that user signs out, so nothing arrives after the session ends ([livetable](/learn/man/livetable)). ## The Page Arrives With Its Data A realtime page needs the current data and every change after it, joined without a gap. With LiveSync, that join is the `sync()` function you write, plus a sequence id that tells the SDK where in the stream the snapshot was taken. An Elements page renders on the server with the rows already in it, so the first paint has data, for people and for search engines. `view()` starts listening before it runs the query, and the browser uses each row's id to drop a message it already has, so a change made while the page loads is never lost and never shown twice ([options](/learn/man/livetable/options)). The rows are stored in your Postgres tables, so there is no retention window to pay for. For a long table, a window keeps one page of rows live and loads older ones on request ([windows](/learn/man/livetable/windows)). ## Optimistic by Default A change should appear on the screen of the person who made it without waiting for the network. Every LiveTable does this with no extra code. The browser gives the new row its id before sending it, shows it at once, and matches the server's broadcast to it by that id. If the server refuses the write, the row is taken back out ([mutations](/learn/man/livetable/mutations)). There is no mutation id to carry through your backend: the row's id does that job. ## The Build Checks Realtime Code An Ably message is a channel name, an event name and a payload. The code that publishes it and the code that subscribes to it agree on all three by convention, and nothing checks that they still agree after either side changes. In Elements, a LiveTable and a channel are declared with a TypeScript type, `LiveTable` or `Channel`, and that type follows the rows from the server into the template. Rename a field and the build reports every template that still reads the old one. That check is what an agent needs. Its work improves through correction, learning about each mistake as it makes it, and Ably asks it to learn layers of integrations instead: the realtime client, the Models SDK with its `sync()` and `merge()`, an outbox table and a database connector, joined by mutation ids it must pass through by hand. Each layer is another API to read and another seam where the two sides can drift. An SDK is a shortcut only for what an agent cannot write quickly from standards, and keeping a page in step with its rows is plumbing every app needs, so Elements ships it in the framework. Past that plumbing it keeps abstractions minimal, and because the framework and its build are one coherent system, a LiveTable goes through the same tests, migrations and program analysis as the rest of the app. The project server runs that build. It runs while you work and holds the build graph and build state in memory, so on every save the build state is updated almost instantly. It builds in milliseconds and answers agents and humans in microseconds, through `elements build -json` ([build](/learn/man/build)). Tests reach realtime code too. A test makes the same LiveTable write the browser makes, through your handler, and it broadcasts the same way, with no account or mock standing in for a messaging platform ([mutations](/learn/man/livetable/mutations), [tests](/learn/man/tests)). ## Presence, Typing and Dashboards Are Your Own Code Ably runs presence as a feature of its network, and each enter, update and leave is [counted as a message](https://ably.com/docs/presence-occupancy/presence). In Elements, presence, typing and dashboards are your own code, in your tables, checked by your build. A channel listener reports when a browser connects and when it leaves: write a row on one and delete it on the other. The [presence](/learn/man/recipes/presence) list then lives in your database, where you can query it. The same channels drive [typing indicators](/learn/man/recipes/typing-indicator), [live dashboards](/learn/man/recipes/live-dashboard) fed by a background job, and a [collaborative canvas](/learn/man/recipes/collaborative-canvas). LiveTable drives [chat rooms](/learn/man/recipes/chat-rooms) and [shared lists](/learn/man/recipes/shared-lists). None of it is metered. ## Moving From Ably An agent can port an Ably app easily, because the realtime layer moves into the app: rows stay in sync in every browser through a [LiveTable](/learn/man/livetable), and every other message goes over a [channel](/learn/man/channel). You keep the live updates your users see, and you gain realtime in the framework itself, with nothing to sync between two systems and nothing metered. ## Questions ### What is the difference between Elements and Ably? Ably is a hosted realtime messaging platform with a base fee plus metered usage, billed per minute or per monthly active user. Elements is an integrated app environment that ships with a default framework, and realtime is part of that framework. LiveTable keeps database rows in sync in every browser, and channels send any other message. Both pass messages through the Postgres your app already uses, and nothing is metered. ### Is Elements an Ably alternative? Yes. LiveTable and channels replace Ably Pub/Sub and LiveSync for web apps built with Elements. They run inside your app on your own servers, so there is no second account, no tokens to issue and no usage bill. ### How does Ably bill for messages? Ably counts a message each time it moves: one message published to ten subscribers counts as 11. On the per-minute model, connections and channels are billed by the minute on top, and persisted history counts messages again ([Ably billing](https://ably.com/docs/platform/pricing)). Elements meters nothing: it is priced per machine. ### How is LiveTable different from Ably LiveSync? LiveSync has your backend write each change to an outbox table, which a connector reads with Postgres LISTEN and forwards to Ably. In the browser, you write sync and merge functions, and optimistic updates need mutation ids passed through your backend. A LiveTable write saves the row and broadcasts it in one call, the page renders on the server with its rows, and optimistic updates are built in. ### Does Elements realtime work across several servers? Yes. Every Elements app server listens on Postgres with LISTEN and NOTIFY, so a message sent from one server reaches browsers connected to any of them. The servers share one Postgres database, such as a managed one. ### How do I move from Ably to Elements? An agent can port an Ably app easily, because the realtime layer moves into the app: rows stay in sync through LiveTables, and every other message goes over an Elements channel. You keep the live updates your users see, and you gain realtime with nothing to sync between two systems and nothing metered.