Self-Hosted Auth for Developers: Better Auth vs Authelia vs Supabase Auth

Self-Hosted Auth for Developers: Better Auth vs Authelia vs Supabase Auth

🇨🇳 阅读中文版:开发者自建认证怎么选:Better Auth、Authelia 与 Supabase Auth 的真实差别

The product was nearly ready for its first outside users. The Next.js pages worked, the API wrote to Postgres, and the team had a rough idea of how accounts would map to workspaces. Then someone asked the question we had managed to postpone: where should login live?

A search for open-source authentication brought up Better Auth, Authelia, and Supabase Auth. All three looked relevant. Yet their setup guides seemed to start in different buildings. Better Auth wanted a place in the application. Authelia wanted to know about the reverse proxy. Supabase Auth wanted us to think about database access. We were about to compare features when it became obvious that we hadn’t agreed on the job we were hiring the software to do.

That is the useful way into this decision. If you are adding accounts to an app you build and deploy, these are three ways to place responsibility for identity. They are not interchangeable login widgets. Nor is this a procurement comparison with Auth0 or Okta, where a vendor operates the identity service for you. Here, even when a platform offers a hosted route, the developer still has to choose how its authentication machinery connects to the app and who maintains the self-hosted parts. The question begins in code, not in an enterprise SSO purchasing meeting.

Follow one request past the login form

Suppose Maya signs in and opens a private project. Someone must establish that she is Maya. Something must keep that fact available on the next request. Then the system must decide whether Maya belongs to this project and whether she can edit it. Authentication, sessions, and authorization are separate jobs, even if the user experiences them as one click followed by a page load.

The distinction matters most when the first authenticated request reaches data. A reverse proxy might confirm that Maya is allowed through the front door but know nothing about which project belongs to her. A session library might identify her inside a Node handler but have no opinion about the SQL query in that handler. A database policy might correctly reject her attempt to read another user’s row, provided the request reaches the database with the right identity and the policy is in force. All three can participate in a working login flow. Only one of them should be treated as the final authority for each decision.

Before installing anything, I draw the path for a normal request: browser, proxy if there is one, application, authentication component, database. I put a mark next to the step that issues or checks a session, and another next to the step that enforces access to a private record. The marks land in different places for these three products. That is more useful than counting checkboxes on their homepages.

Choice Where it sits What your application connects to Where data authorization usually belongs
Better Auth In the TypeScript application stack Its own auth configuration, handlers, sessions, and plugins Your server-side business rules and database access code
Authelia A separate SSO/MFA portal, commonly paired with a reverse proxy; also an OIDC identity provider The proxy’s access checks or an application OIDC integration Your application, beyond any entrance policy
Supabase Auth Supabase’s GoTrue-based auth module Auth APIs/SDKs and, when used together, Supabase Postgres Postgres Row Level Security plus application rules

The table does not rank security or ease of use. It predicts where your team will be debugging at 2 a.m. A cookie problem in an app-owned session flow sends you to the application and browser. An unexpected redirect through a gateway sends you to proxy rules and identity-provider configuration. A signed-in user who sees no rows in a Supabase client might have a correct login and an incorrect RLS policy. Calling all three incidents “auth is broken” obscures the repair.

Better Auth fits when login is part of your app

Our original project was a single customer-facing Next.js application backed by a database we already managed. Users needed to register, return to an existing account, and eventually join a shared workspace. The team reviewed TypeScript changes together and deployed the app as one unit. In that setting, Better Auth was the most direct thing to prototype: an open-source TypeScript authentication framework whose plugin architecture offers paths for features such as two-factor authentication and organizations.

Its appeal is proximity. The integration lives alongside application code, so a change to sign-in behavior can go through the same review and release process as the feature that needs it. A route handler can establish the user from a session and pass that identity into business logic. When a workspace invitation is accepted, application code can connect the authenticated user to the appropriate records. You are not maintaining an entirely separate portal simply to establish who is calling your one app.

Proximity also makes it easier to see where the framework stops. Better Auth can give you an authenticated identity and facilities for common account flows. It cannot infer your product’s rule for editing an invoice, approving another member, or reading a project that moved between organizations. An organizations plugin can help model membership; it does not absolve you of deciding what membership permits in your own endpoints. Treating an authenticated session as permission to access every resource is a bug the library cannot fix for you.

The unglamorous parts remain yours as well. You need a working database setup, appropriate secrets, reliable email delivery for flows that use email, and cookie and callback settings that behave on the domains you actually deploy. A local demo where sign-in succeeds says little about what happens after a domain change or when an account recovery message never arrives. The app team’s normal operational habits become the auth team’s operational habits, because they are the same people and usually the same deployment pipeline.

That trade can be excellent for a small team. A developer tracing a failed request can read the application logs, look at the session handling, and inspect the authorization code that guards the record. The chain is compact enough to reason about. It is also a reason to write focused tests rather than assuming the framework’s existence makes the product secure. Create two users, put a private object under one account, and try to read or modify it through the other account’s API session. Repeat without a session. If either request succeeds, adding a nicer login screen will not help.

Better Auth becomes a less obvious choice when the application does not really have a TypeScript center. A polyglot system can integrate with a TypeScript service, but adding a new Node service solely to accommodate an auth framework changes the architecture you were trying to simplify. You would now need to define how every other service trusts it and how those services receive a stable user identity. That may be justified; it is not the effortless version of “just add a package.” Ask whether the team already wants application-owned authentication in its TypeScript stack before deciding that this framework solves a cross-language identity problem.

There is another temptation: use the framework’s extensibility as a reason to implement features nobody has requested yet. We nearly fell into that. For the first release, the useful exercise was getting one complete path right: create an account, sign in, read one protected project, sign out, and verify the old session can no longer read it. The plugin catalog mattered only after we knew which account behaviors the product actually needed. Choosing an expandable framework and designing every possible account feature are different activities.

Authelia is at the entrance to a collection of services

Now picture the same team six months later. Alongside the product there is an internal dashboard, a documentation server, a monitoring UI, and an admin tool. Each has a URL; not all were written by the team; some have poor login options of their own. The operational question has changed. Instead of embedding a registration flow in one product, the team wants a controlled entry point and perhaps a common sign-in across services.

Authelia belongs in this conversation. It is an open-source SSO and MFA portal and an OpenID Connect identity provider. A common deployment puts it alongside Nginx or Traefik so a reverse proxy can ask whether a request should pass. An application that supports OIDC can also integrate with Authelia as an identity provider. These are different integration modes, but both place the identity system outside the individual app’s auth library. Its support for post-quantum cryptography may matter to a team with that requirement; it does not change the first architectural question of where the access decision is made.

There is something satisfying about putting a gate in front of several tools. You can stop unauthenticated traffic before it reaches a protected service and manage entrance policy in one place. For an internal dashboard whose only meaningful distinction is “authorized staff may open this,” that can remove repeated, mediocre login implementations. The team already operating proxies and containers may prefer a dedicated service to custom authentication code scattered across tools it does not control.

But a gate is not the same thing as an application’s user model. If an admin portal handles records for several customers, an entrance check cannot decide which customer’s records each operator may change unless the application has enough trusted identity information and enforces those rules itself. Likewise, putting Authelia in front of a customer-facing Next.js app does not make workspace invitations, tenant separation, or per-project permissions appear. The portal knows who was admitted; the product must still know what that person may do.

The trust boundary deserves special attention when the proxy passes identity information to an app. If the app blindly accepts an identity header from any caller, a direct connection that bypasses the proxy can turn that header into an impersonation route. Restrict direct access to the app, ensure the proxy controls and sanitizes the relevant headers, and test the bypass case rather than trusting a diagram. With OIDC, the application has protocol work of its own: it must handle the sign-in flow and validate what it receives according to its integration. In neither mode does a successful redirect prove that authorization inside the app is correct.

Authelia also brings a new failure domain. The portal, its backing configuration and state, the proxy rules, certificates, and callback paths need maintenance. If the gateway is unavailable, the services that depend on it may be unavailable to users too. A team that already runs shared infrastructure can absorb this as part of its platform work. A team with one application and no proxy expertise might be buying a longer incident path merely to avoid keeping auth code in the app. The separation helps when it consolidates access control across several services; it is overhead when introduced for a login form alone.

Single sign-on can also be confused with a product’s organizations feature. SSO lets an identity move between applications without an unrelated login at every door. An organization in a SaaS product is a container for customers, membership, roles, and data. An employee allowed into three internal tools does not thereby acquire a role in a customer’s workspace. Those concepts may eventually be connected, but doing so requires explicit mapping and policy, not a checkbox labeled SSO.

For that reason, I would not replace a simple app-level account design with Authelia because both products happen to appear under “self-hosted auth” in search results. I would bring it into the architecture review when multiple services or an existing proxy-based access policy create a specific need. If the team runs both internal tools and a customer product, the internal tools may sit behind Authelia while the customer product uses another login design. Forcing both audiences through one session model can make logout behavior and support harder to explain without giving either audience a useful improvement.

Supabase Auth makes more sense when Postgres is already part of the plan

There is a third version of our project. Suppose the app’s data already lives in Supabase Postgres, the client uses Supabase APIs to read some records, and the team intends to enforce ownership in Row Level Security policies. In this version, Supabase Auth has an unusually short route from “user signed in” to “database knows which user’s rows this request may read.” It is Supabase’s built-in authentication module, based on GoTrue, and its tight connection to Postgres and RLS is the reason to evaluate it first.

Take a notes table with an owner column. A frontend filter for the current user’s ID makes the interface look right, but a client can change a request. If clients can address the data API, the database needs to reject a query for someone else’s notes regardless of what the UI displays. An RLS policy can use the authenticated request’s user context to constrain which rows are accessible. Now the security boundary sits at the data layer rather than depending on every page remembering to add an owner filter.

The interesting shift is that the login implementation and the data-access design must be reviewed together. A policy that permits reads but accidentally allows broad updates is incomplete. A policy attached to one table does not protect an unrelated table exposed through the same path. A service using elevated database privileges may not behave like an ordinary signed-in user. The first successful sign-in tells you that the identity flow works; it does not certify all the RLS rules. You still have to check each operation the app exposes, including writes, and distinguish user-facing requests from privileged background jobs.

This is where Supabase Auth can save a great deal of glue code for a team that has already chosen the platform’s data path. Identity, client requests, and row policies can be designed as parts of one request flow. A developer debugging an empty notes screen can inspect the signed-in user context and the relevant policy instead of inventing a separate ownership check in every frontend component. The convenience is real because the pieces meet at a well-defined database boundary, not because authentication magically grants correct permissions.

It is possible to use Supabase Auth without moving all your application data into Supabase. That flexibility is useful, but it changes the argument for choosing it. If your existing Node API owns a separate database and every sensitive query happens in that API, you still need to associate Supabase identities with your own records and enforce business rules there. RLS in Supabase Postgres cannot protect tables that live elsewhere. In that architecture, decide whether you want Supabase’s auth APIs and account workflow on their own merits. Do not count data-layer integration as a benefit you are not using.

Self-hosting changes the operational equation again. Running Supabase Auth as part of a self-hosted Supabase setup is more involved than adding a TypeScript framework to an existing app. The surrounding platform and its dependencies need configuration, updates, backups, and attention when sign-in email or data access fails. Teams already using Supabase may find that a reasonable extension of their current operating model. Teams considering an entire backend stack solely to get a login page should calculate the work after the initial demo, not only the time it takes to start one.

The question is not whether database-enforced access is universally better than application-enforced access. Many products need both: RLS to guard rows reached through a client data API, and application logic for workflows that cannot be expressed as a simple row policy. A manager approving a transfer between accounts, for example, involves more than proving that the manager owns one row. Choose the boundary that protects the paths your application actually exposes, then test those paths with users who should and should not have access.

The failure you expect to debug is a clue

When our team discussed these tools, feature lists were a distraction. We made faster progress by asking where we would look if a customer reported, “I signed in, but I can’t see my project.” With Better Auth, we would start at the application session and the endpoint’s business authorization. With Authelia, we might first check the proxy and portal if access never reached the app, then inspect the app’s user mapping if it did. With Supabase Auth, the request’s user context and the table’s RLS policies would be immediate suspects. The same user-facing complaint has a different owner in each architecture.

Other incidents sharpen the distinction. If password recovery mail does not arrive, who owns delivery and support? If a callback only fails on the production domain, which component configured that domain? If a former member retains access, where is membership checked and how does a changed membership affect an existing session? These questions are not a request to predict every future outage. They force us to name the component that will have to answer when the feature leaves the tutorial and meets actual users.

Popularity cannot answer them. Better Auth’s repository has around 30,000 GitHub stars; Authelia’s has around 29,000. Those figures indicate interest, not suitability for your request path. A well-maintained identity provider can still be the wrong starting point for a single app; an application framework can still be the wrong tool for a fleet of unrelated services. Compare the responsibility you need to assign before comparing project momentum.

The starting architecture usually narrows the choice. For a Next.js or Node app with its own database and a team comfortable owning authentication in TypeScript, prototype Better Auth and make the product’s authorization rules explicit. For a set of services already behind a reverse proxy, explore Authelia and map which services need only an entrance check and which need an actual OIDC identity inside. For an app using Supabase Postgres and client data access, test Supabase Auth together with RLS rather than treating login and data permissions as separate tickets. None of those recommendations is a feature ranking; each follows the request through the system already in place.

A two-product answer is sometimes sensible. An internal administration interface behind Authelia and a customer application using Better Auth, for example, serve different people and have different access rules. The arrangement becomes less sensible if the two sign-ins are stacked on the same customer journey without a clear reason. Which session controls logout? Where does the customer change credentials? Which identity owns the account in the database? If the team cannot answer those questions, the extra layer has probably made the experience harder without solving an access problem.

Draw the flow, then make it fail safely

I ask developers to sketch three journeys before committing to a tool. First, a new user creates an account. Second, that user signs in after closing the browser and requests a private record. Third, a different signed-in user requests the same record directly from an API, skipping the interface. On the sketch, name who issues the session, who validates it, how the app learns the user ID, and where the last request is denied. If any arrow just says “auth happens,” there is unfinished design work at that boundary.

Then run the journeys against a tiny implementation. Do not stop at a successful redirect to a dashboard. Confirm that the user can read their own record, a stranger cannot read or mutate it, and sign-out makes the old session unusable for protected requests. For a proxy arrangement, attempt to reach the application without going through the proxy. For Supabase client access, inspect the relevant RLS policies and test both permitted and rejected operations. For an app-level framework, verify session and callback behavior on the production domain as well as locally. The small test is designed to reveal the hole between two components, not to demonstrate how many features you installed.

Finally, put operations on the same sketch. Mark where secrets are held, where account email is sent, what must be backed up, and who upgrades the authentication component. Open-source software can be self-hosted without a mandatory commercial license fee, but someone still keeps the login path working. Better Auth places much of that work near the app team. Authelia adds a shared gateway to operate. Supabase Auth ties the experience closely to the backend and its policies. The right choice is the one whose failure modes your team can see and repair.

For our original one-app Next.js project with self-managed Postgres, I would build a small Better Auth integration first and keep project permissions in explicit server-side checks. If the database and client data path were already on Supabase, I would start instead with Supabase Auth and test RLS from the first private table. I would bring Authelia into the discussion when several services needed a shared entrance. The login form is the visible part; the placement of identity and permission checks is the decision that stays with you.

Sources: Better Auth documentation, Authelia documentation, Supabase Auth documentation.

Related reading

Browse the full guide →

Stay updated with our latest AI insights

Follow FuturePicker on Google
Scroll to Top