Elements vs Auth0

Markdown

Auth0 is a hosted identity company, owned by Okta (Auth0 overview). An app sends its users to login pages that Auth0 hosts. Auth0 signs them in against accounts kept in its own user store and sends them back to the app (Universal Login). Auth0 has SDKs for Next.js and many other frameworks. Auth0 sells identity as a subscription: plans are priced by monthly active users, and features unlock at each of four tiers (Auth0 pricing).

Elements is not an identity 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).

The difference is where sign-in lives. With Auth0, it is a separate subscription: your users sign in on Auth0's pages, and their accounts are kept in Auth0's tenant. Auth0 prices it by how many of them sign in each month. In Elements, sign-in is part of your app. Users sign in on your own page, and their accounts are rows in your own Postgres. The page updates the moment they sign in. Nothing on the Elements bill is counted per user.

At a Glance

Elements Auth0
What it is An integrated app environment with sign-in built into its default app framework A hosted identity service you add to an app
Where users sign in A page in your app (recipe) Login pages hosted by Auth0, reached by redirect
Where user accounts live A users table in your own Postgres Auth0's user store
Sessions One: a row in your own Postgres, checked on every request Two: one on Auth0's login server, and a cookie in your app set by Auth0's SDK (session layers)
Using your own database for users Built in: users live there from the first migration Custom database connections, on the Professional and Enterprise plans
What you pay for Tooling, per machine (pricing) Monthly active users, on one of four plans (pricing)
Sign-in in development Runs against the bundled Postgres on your machine Goes through your Auth0 tenant, with callback URLs registered in Auth0

Next.js Sends You Out for Sign-In

Almost every app needs users to sign in. Next.js does not include sign-in: its authentication guide recommends an authentication library and lists twelve, Auth0 among them. Auth0's Next.js quickstart installs @auth0/nextjs-auth0, sets five environment variables, creates an Auth0 client in lib/auth0.ts and a proxy.ts file that mounts /auth/login, /auth/callback, /auth/logout and three more routes, and adds client components for the login and logout buttons.

Sign-in arrives as a second vendor, with a tenant to configure in Auth0's dashboard, secrets to manage and its own pricing.

Elements starts with it. Sessions are part of the app framework, and sign-in is one server function that checks a password hash in Postgres and calls session.login():

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 @rpc comment makes it a server function the browser calls like any other function, with typed arguments (rpc). The manual's authentication recipe builds the signin and signup pages around it.

Users Sign In on Your Page, Not on Auth0's

Where a user signs in shapes the experience of your app. With Universal Login, your app hands the user to "Universal Login, hosted on Auth0's Authorization Server," and gets them back on a callback route. The login pages are customized through Auth0, and they are served from Auth0's domain unless you configure a custom domain.

In Elements, the signin page is a page in your app, written in the same html language as every other page, styled by the same CSS, and checked by the same build. The form calls the signin server function directly. When it returns, the session is live on the page: anything that reads it updates at once, with no trip to another domain and no reload.

import { session } from "@elements/app";

<nav>
  <span e:if={session.isLoggedIn()}>{session.get("userName")}</span>
  <a e:else href="/signin">Sign in</a>
</nav>

The same session object works in route handlers, server functions and templates, over HTTP and over the WebSocket each page keeps open (session). Its fields are declared once in TypeScript, so reading one that does not exist is a compile error.

Your Users Are Rows in Your Database

Auth0 keeps your users in its own user store. To authenticate against a database of your own, Auth0 offers custom database connections, where you write "database action scripts, which are Node.js functions that Auth0 calls during functionality like logins and password changes." Even then, Auth0 creates a user account of its own after the first login, and the feature starts at the Professional plan (Auth0 pricing).

In Elements, your users live in your own database from the first migration. The users table has the columns your app needs, next to the rest of your data, and a question about your users is a SQL query:

select email
  from users u
 where createdAt > now() - interval '7 days'
   and not exists (select 1 from projects p where p.ownerId = u.id);

elements db runs it on your machine, and elements db -remote=production runs it on your server (elements db). A page that lists a team, or a report on signups, joins users like any other table, with no API call and no copy to keep in sync.

Sign-In Runs on Your Laptop and in Your Tests

Developing against Auth0 means developing against a tenant. Every URL Auth0 may send a user back to after sign-in has to be on the application's list of allowed callback URLs in Auth0 (application settings), and each sign-in makes a round trip to Auth0's servers.

In Elements, sign-in is your own server function over your own table, so on your laptop it runs against the bundled Postgres, with no tenant, no callback URL and no secret to set. The same holds in tests. A test can call the real signin function against real rows, and any other test signs in with one session.login() call. Each test runs in a transaction that rolls back, so there is no identity service to mock and no test user to clean up (fast tests).

For an agent, an identity SDK is not where the value is. The value is in correction: being told about a mistake the moment it is made. An agent adding Auth0 has to learn @auth0/nextjs-auth0, its environment variables and its mounted routes, plus tenant settings that live in Auth0's dashboard, where nothing in the app's build can see them. An SDK earns its place only on code an agent cannot write quickly from standards, and checking a password and setting a session is a short function in SQL and TypeScript. Elements keeps sign-in to that function and session.login(), and since the framework and its build are one coherent system, sign-in goes through the same tests, migrations, type checking and program analysis as every other route. The project server runs while you work. On every save the build state is updated almost instantly, so a broken sign-in reaches you and your agent from elements build -json in microseconds (build).

One Sign-In Path, One Session

The case for Auth0 is that identity is security code, and security code is easy to get wrong. Elements agrees, which is why sessions are part of Elements and not something your app assembles from libraries.

An Auth0 sign-in crosses two systems. The user leaves your app for Auth0's login server and comes back through a callback. From then on, Auth0 keeps a session of its own on its authorization server, your application tracks a second one, and the two have to stay in step (session layers).

An Elements sign-in stays on one path: your page, your server function, your Postgres.

  • One session, checked every time. Every request checks the session against your own database, so signing out ends it at once (session). There is no second session to expire or revoke separately.
  • Sign-out takes effect everywhere in the app. Deleting the row ends the session. Every LiveTable view and channel listener opened under it stops, so the next person at the keyboard never sees the previous user's rows update.
  • Every device is visible. session.findActiveSessions() lists a user's sessions across devices, and session.revoke() signs one out from your own account page.
  • Passwords are hashed where they are stored. The recipe hashes with bcrypt through the pgcrypto extension that Elements installs, and compares hashes inside Postgres (authentication recipe).

Who may do what stays in your code, next to the data it protects. The manual has recipes for role-gated admin pages and email confirmation at signup.

No Per-User Bill

Auth0 bills by monthly active user, which it defines as "any non-internal (non-employee) user that authenticated during a given month for a given tenant." Its four plans add features at each tier: Pro multi-factor factors start at Essentials, and custom database connections start at Professional (Auth0 pricing). As an app grows, both its user count and the plan it needs push the bill up.

Elements charges for its tooling, per machine, and meters nothing (pricing, licenses). Users, sessions and sign-ins never appear on the bill, and keeping users in your own database is not a plan feature: it is where they live from the start.

Questions

What is the difference between Elements and Auth0?

Auth0 is a hosted identity service owned by Okta: users sign in on Auth0's hosted pages, their accounts live in Auth0's user store, and pricing is by monthly active user. Elements is an integrated app environment whose default framework has sessions built in, so users sign in on your own pages and live in your own Postgres, and nothing is billed per user.

Is Elements an Auth0 alternative?

Yes. An Elements app needs no identity tenant: sign-in is a page and a server function in the app, and the session is a row in its own Postgres. The manual's authentication recipe walks through the signin and signup pages.

Does Elements redirect users to a hosted login page?

No. The signin page is a page in your app, and the form calls a server function directly, so the user never leaves your site. When it returns, the session is live and anything that reads it updates without a reload.

Can I keep users in my own database instead of Auth0?

With Elements, that is where they live. The users table comes from one of your own migrations, and sessions are rows in the elements.sessions table in the same Postgres, with no custom connection scripts to write.

How is Auth0 priced compared to Elements?

Auth0 charges by monthly active user, on four plans that add features at each tier (Auth0 pricing). Elements charges for its tooling, per machine, and meters nothing, so your user count never changes the bill (pricing).