@slices.sh/auth.betterauth
v0.3.4
Published
slices v2: the auth service, backed by Better Auth inside the app. Memory here; MongoDB, Neon, Supabase and AuthLocker each in a package of their own.
Readme
@slices.sh/auth.betterauth
Sign-in, sessions, passkeys, two-factor and organizations run inside the app, on Better Auth.
This is v1's services/auth/BACKEND.md, for the package.
export default auth({
provider: betterauth({ database: "memory", methods: ["password"] }),
})This package holds no database driver and no AuthLocker client. Each is a package of its own, which the config imports and passes in, so an app installs only what its configuration uses:
| Package | Passed as | Brings |
|---|---|---|
| @slices.sh/auth.betterauth.mongo | database: mongo() | mongodb, @extraorbital/mongo |
| @slices.sh/auth.betterauth.neon | database: neon() | @neondatabase/serverless, drizzle-orm, @extraorbital/neon |
| @slices.sh/auth.betterauth.supabase | database: supabase() | pg, drizzle-orm, @extraorbital/supabase |
| @slices.sh/auth.betterauth.authlocker | codes: authlocker() | @authlocker/better-auth, @extraorbital/authlocker |
import { mongo } from "@slices.sh/auth.betterauth.mongo"
import { authlocker } from "@slices.sh/auth.betterauth.authlocker"
export default auth({
provider: betterauth({ database: mongo(), methods: ["email-otp", "google"], codes: authlocker() }),
})Only the ways in listed in methods are served: an app without password has no
/api/auth/<config>/sign-up/email, and one without passkeys (passkeys, or passkey among
methods) has no WebAuthn endpoints. Two-factor needs password sign-in: the authenticator app is
the second factor to a password, so twoFactor without password fails when the config loads.
Email and SMS codes, Google and GitHub go through AuthLocker: listing one without codes fails
when the config loads too, and says which package to install. So does a config from 0.2 that
names its database as a string (database: "mongo").
Environment
| Variable | What it is |
|---|---|
| BETTER_AUTH_SECRET | Signs every session. Generated into .env.local by the setup item; changing it signs everyone out. |
| BETTER_AUTH_URL | Optional. The public origin, such as https://acme.com: callback URLs and the passkey domain come from it. Unset, it is the deployment's own on Vercel (VERCEL_PROJECT_PRODUCTION_URL in production, the branch and deployment URLs in a preview), and in development the origin each request comes to, on localhost only. Set it behind a proxy or on another host. |
| MONGODB_URI, MONGODB_DB | database: mongo(). MONGODB_DB defaults to the database in the URI, then app. |
| POSTGRES_URL, POSTGRES_URL_NON_POOLING | database: neon(). The second, the direct connection, is optional and only migrations use it. |
| SUPABASE_DB_URL (with SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY) | database: supabase(). |
| AUTHLOCKER_CLIENT_ID, AUTHLOCKER_CLIENT_SECRET | Email codes, SMS codes, Google, GitHub. Server only. |
| AUTHLOCKER_ISSUER | Optional. Defaults to https://authlocker.dev. |
| AUTHLOCKER_CAPABILITIES | Optional. socials.google,socials.github,otp.email,otp.phone. The server refuses any way in not listed here and in methods. |
Register {BETTER_AUTH_URL}/api/auth/<config>/callback/google and /github with your AuthLocker
client; a localhost origin needs no registration. AuthLocker enforces the limits the screen shows:
a code lives 10 minutes and allows 5 wrong attempts; a new code comes no sooner than 60 seconds
after the last, 3 times per verification; SMS needs a claimed client and stops at its daily cap.
Storage
- memory: everything lives in this process and is gone on restart. It refuses to run in
production unless
BETTER_AUTH_URLis a localhost origin. - mongo: collections are created on first write, so there is no migration. The
namespaceoption is the MongoDB database. - neon, supabase: tables live in the
namespacePostgres schema (slices_auth), outside the schemas Supabase exposes. Runnpm run db:migrateafter the URL is set; it generates and applies Drizzle migrations fromservices/auth/schema.ts. All tables, organizations included, are in the schema, so turning a feature on later needs no new migration.
Invitations and domains
Invitations are stored, not emailed, until sendInvitationEmail is given: the cards offer a link
to share. A recipient must sign in with that verified email before accepting or declining it.
An owner or admin whose verified email matches a private domain can claim it (public mail
providers are refused); a verified sign-in joins matching organizations once. Password and SMS
sign-ups do not verify an email and never auto-join.
The profile
Name and picture can always be changed. The address needs proof: a code sent to the new
address through AuthLocker (email-otp in methods), or a link through sendChangeEmailVerification.
The number needs phone-otp.
Account deletion
The core delete endpoint verifies a password or requires a fresh session. An account that is an organization's only owner is refused, across all membership pages. Passkeys, second-factor records, memberships and invitations issued by the deleted user are cleaned up. Application records and uploaded files belong to the host's deletion lifecycle.
Access requests
With organizations, one durable request is stored per organization and account; only that
organization's owners and admins read or decide them, and approvals always grant member through
Better Auth's addMember. Approvals use a durable claim before the membership write; a
process interruption can leave a request approving, and an explicit retry after five minutes
rechecks membership first.
