# Elements vs Clerk Clerk is a hosted authentication company. An app adds Clerk's SDK and its prebuilt sign-in components, and Clerk keeps the user accounts and sessions in its own service, where the app reads them through Clerk's APIs ([Clerk docs](https://clerk.com/docs)). Clerk has SDKs for Next.js, React and other frameworks. Clerk sells sign-in as a subscription: paid plans charge a monthly fee plus a charge for each monthly retained user past the plan's allowance, and some features are sold as add-ons ([Clerk pricing](https://clerk.com/pricing)). Elements is not an authentication service. It is an integrated app environment for building web apps, built for people and their agents: a project server that runs while you work, a package installer, a test runner, a bundled Postgres database, and a deploy command for any Ubuntu server you reach over SSH. It ships with a default app framework, and sign-in is part of it. The framework covers routing, server rendering, typed server functions, sessions, realtime data, background jobs and email. Elements charges for that tooling, per machine, and meters nothing ([pricing](/pricing)). The difference is who holds your users. With Clerk, sign-in is a second vendor. Your users are accounts in Clerk's service: your app reads them over the network, and copies them into its own database by webhook when a page needs them there. In Elements, your users are rows in your own `users` table, and sessions are rows in the same Postgres. One session API works in routes, server functions and templates. Nothing on the Elements bill is counted per user. ## At a Glance | | Elements | Clerk | |---|---|---| | What it is | An integrated app environment with sign-in built into its default app framework | A hosted sign-in and user management service you add to an app | | Where user accounts live | A `users` table in your own Postgres | Clerk's service | | Where sessions live | A table in your own Postgres, checked on every request | Clerk's service, with a signed token in a cookie | | What you pay for | Tooling, per machine ([pricing](/pricing)) | A plan, plus each monthly retained user past the plan's allowance, plus add-ons ([pricing](https://clerk.com/pricing)) | | Users in your queries | A join in SQL | A copy kept in sync by webhooks, or a call to Clerk's API | | Sign-in pages | Pages in your app, styled by your own CSS and checked by your build ([recipe](/learn/man/recipes/authentication)) | Clerk's components, styled through Clerk's appearance settings ([appearance](https://clerk.com/docs/guides/customizing-clerk/appearance-prop/overview)) | | Sign-in in development | Runs against the bundled Postgres on your machine, with no keys | Calls Clerk's service, with API keys in `.env.local` | ## Sign-In Is Part of the App, Not a Second Vendor Almost every app needs users to sign in. Next.js does not include sign-in: its [authentication guide](https://nextjs.org/docs/app/guides/authentication) recommends an authentication library and lists twelve, Clerk among them. Clerk's own [Next.js quickstart](https://clerk.com/docs/nextjs/getting-started/quickstart) writes two API keys to `.env.local`, adds a `proxy.ts` file that runs `clerkMiddleware()` from `@clerk/nextjs`, and wraps the app in ``. Hosted app builders reach for the same service: when a Replit user asks for auth, Replit provisions Clerk for them ([Clerk for Platforms](https://clerk.com/platform)). Either way, sign-in arrives as a second vendor, with its own dashboard, its own API keys and its own pricing. Elements starts with it. [Sessions](/learn/man/session) are part of the app framework, and sign-in is a server function that checks a password hash in Postgres and calls `session.login()`: ```ts // app/shared/services/auth.ts import { sql, session, AuthError } from "@elements/app"; /** @rpc */ export function signin(email: string, password: string) { let user = sql<{ id: string }>(` select id from users where email = ${email.trim().toLowerCase()} and passwordHash = crypt(${password}, passwordHash) `).first(); if (!user) { throw new AuthError("invalid email or password"); } session.login({ userId: user.id, userName: email }); } ``` The [authentication recipe](/learn/man/recipes/authentication) in the manual builds the signin and signup pages around it, down to the input attributes that make password managers offer to save and fill. ## Your Users Are Rows in Your Database Clerk keeps your users in its own service, so when your app needs them in its own database, you copy them there. Clerk's [syncing guide](https://clerk.com/docs/guides/development/webhooks/syncing) sets that up with webhooks, and it warns that "webhook deliveries are not guaranteed and may occasionally fail," and that there "can be a delay between when a Clerk event occurs and when the corresponding data is reflected in your database." The same guide explains when an app has to sync anyway: Clerk's frontend API gives a browser the currently signed-in user and no one else, so any page that shows other users, such as a team list or a comment thread, needs a copy. In Elements, there is nothing to sync. Users are a table you create in a [migration](/learn/man/migrations), with the columns your app needs, next to the rest of your data. Reading the signed-in user's account is a join: ```ts // app/shared/services/account.ts import { sql, session } from "@elements/app"; interface Account { email: string; plan: string; projects: number; } /** @rpc */ export function myAccount(): Account { let userId = session.getOrThrow("userId"); return sql(` select u.email, u.plan, count(p.id)::int as projects from users u left join projects p on p.ownerId = u.id where u.id = ${userId} group by u.id `).firstOrThrow(); } ``` A question about your users is a SQL query, answered by your database in one step. `elements db` opens psql on your machine, and `elements db -remote=production` opens it on your server ([elements db](/learn/man/database/cli)): ```sql select email from users u where createdAt > now() - interval '7 days' and not exists (select 1 from projects p where p.ownerId = u.id); ``` ## Sessions That Reach Your Live Data A hosted sign-in service answers one question: who is signed in. Your app's realtime connections, and the live data they carry, have to take that answer and check it again on their own. In Elements, the session and the live data are part of the same framework. The same [session](/learn/man/session) object works in route handlers, `@rpc` server functions and templates, over HTTP and over the WebSocket every page keeps open. A server function called over the socket sees the same signed-in user as the request that rendered the page. A template that reads the session re-renders the moment `session.login()` returns, with no reload. This header swaps from the sign-in link to the user's name: ```ehtml import { session } from "@elements/app"; ``` Two more things follow from the session being part of your app: - **Sign-out ends live data.** Every [LiveTable](/learn/man/livetable) view and [channel](/learn/man/channel) listener is stamped with the session that opened it. When that session ends, the server stops them, so the next person at the keyboard never sees the previous user's rows update. - **Sessions are yours to manage.** `session.findActiveSessions()` lists a user's sessions across devices, `session.revoke()` signs one out, and expiry and automatic renewal are two settings in `config.jsoc`. ## Sign-In Is Tested Like the Rest of Your App Sign-in code needs tests like any other code, and tests against a hosted service need keys, a network and a test instance, or a mock that stands in for it. In Elements, sign-in is your own server function over your own table, so it runs on your machine against the bundled Postgres, with no keys and no round trip to a sign-in service. [Tests](/learn/man/tests) call the real `signin` function against real rows, with one case per outcome: the right password, the wrong one, an unknown email. Every other test signs in by calling `session.login()` directly. Each test runs in a transaction that rolls back, so there is nothing to mock and nothing to clean up ([fast tests](/learn/man/tests/fast)). With Clerk, an agent writing sign-in works through `@clerk/nextjs`, its middleware, its provider component and Clerk's APIs, each a surface to look up and a seam between your app and Clerk's service. An SDK saves an agent time only on code it cannot write quickly from standards, and a sign-in function over a users table is a few lines of TypeScript and SQL. What the agent needs instead is correction: learning what it got wrong the moment it gets it wrong. In Elements those lines live in the app, and the framework and its build are one coherent system, so tests, migrations, type checking and program analysis cover sign-in like any other code. The project server runs while you work. On every save the build state is updated almost instantly, so a failing sign-in test or a misspelled session field reaches you and your agent from `elements build -json` in microseconds ([build](/learn/man/build)). ## Sign-In Is Security Code. Elements Wrote It. The case for Clerk is that authentication is security code, and security code is easy to get wrong. Elements agrees, which is why the session layer is part of Elements and not something you assemble. - **Sessions are checked on the server.** Every request checks the session against your own database, so signing out ends it at once ([session](/learn/man/session)). - **Password hashes never leave Postgres.** The recipe hashes passwords with bcrypt at a work factor of 12, through the `pgcrypto` extension that Elements installs. The signin query compares hashes inside the database, so your TypeScript never sees the stored hash ([authentication recipe](/learn/man/recipes/authentication)). - **The compiler guards the boundary.** Calling `session.login()`, `sql()` or `tx()` from code that runs in the browser is a compile error at the call site ([rpc](/learn/man/rpc)). - **Queries cannot be injected.** The build turns every `${value}` in a `sql` query into a query parameter ([sql](/learn/man/database/sql)). Clerk's prebuilt components save writing the sign-in form, and the form is the small part. In Elements it is two pages from the recipe, in the same html language and CSS as the rest of your app. The rules for who may do what stay in your code too, with recipes for [role-gated admin pages](/learn/man/recipes/admin-roles) and [email confirmation at signup](/learn/man/recipes/signup-confirmation). ## No Per-User Bill Clerk prices by the user. Past the allowance on the Pro and Business plans, each monthly retained user adds a monthly charge, and B2B authentication, administration and enterprise connections are add-ons priced on their own ([Clerk pricing](https://clerk.com/pricing)). Elements charges for its tooling, per machine, and meters nothing ([pricing](/pricing), [licenses](/learn/man/licenses)). Users, sessions and sign-ins never appear on the bill, because they are rows in a database you already run. ## Questions ### What is the difference between Elements and Clerk? Clerk is a hosted authentication service: your app adds its SDK, and your users live in Clerk's service. Clerk bills per monthly retained user past the plan's allowance. Elements is an integrated app environment whose default framework has sessions built in, so your users are rows in your own Postgres and nothing is billed per user. ### Can I replace Clerk with Elements? Yes. Instead of Clerk's SDK, components and keys, an Elements app signs users in with a server function and two pages from the manual's [authentication recipe](/learn/man/recipes/authentication), and keeps the session in its own Postgres. ### Do I need a separate auth service with Elements? No. Sessions ship with the default app framework, with one API in route handlers, server functions and templates. A page that reads the session updates as soon as the user signs in. ### Does Elements sync users from an auth provider? There is nothing to sync. Your users table is created by one of your own migrations, and SQL joins it to your other tables. Sessions are rows in the elements.sessions table beside it. ### How is Clerk priced compared to Elements? Clerk charges a plan fee plus a charge for each monthly retained user past the plan's allowance, with add-ons for some features ([Clerk pricing](https://clerk.com/pricing)). Elements charges for its tooling, per machine, and meters nothing, so your user count never changes the bill ([pricing](/pricing)).