@repo/auth
Introduction
This library is designed specifically for use in a monorepo. It is a simple wrapper that handles access control, using the principles of Gates to systematically and securely grant or deny access.
This library was inspired by the authorization methods from Laravel
Installation
npm install @repo/authAuthentication screens
AuthShell provides the shared card-based shell for sign-in, sign-up, and recovery forms. Applications keep ownership of their auth client and callbacks.
<script lang="ts">
import Github from '@iconify-svelte/simple-icons/github'
import { AuthShell } from '@repo/auth'
const signInWithGithub = () =>
authClient.signIn.social({
provider: 'github',
callbackURL: '/dashboard',
})
</script>
{#snippet form()}<SignInForm />{/snippet}
{#snippet footer()}<a href="/sign-up">Create an account</a>{/snippet}
<AuthShell
title="Sign in"
{form}
{footer}
socialProviders={[
{
icon: Github,
label: 'Continue with GitHub',
onSignIn: signInWithGithub,
},
]}
/>Add another object to socialProviders for each additional provider. AuthShell owns the icon placement, label, button, and separator so every provider remains visually consistent. Pass a separator snippet to replace the default Or continue with email text.
Sessions
Sessions is the state of the current entity that is using the application. Usually this is a user.
Getting sessions
There are two ways of getting a session:
import { getSession } from '@repo/auth/server'
const session = await getSession()You can also directly get the user session. This means that the user object is not null. It will throw a 401 error if it is empty.
import { getUserSession } from '@repo/auth/server'
const session = await getUserSession()System session
Sometimes you want to overwrite the session so that other repositories can still use getSession or getUserSession. This can be done by using useSystemSession
import { useSystemSession, getSession } from '@repo/auth/server'
await useSystemSession(organization.id, user.id, async () => {
const session = getSession() // This is now a system session
})Session users always include a non-null locale, defaulting to en.
The global auth tables include Better Auth's two-factor verification and lockout fields. Generate the app's global Drizzle migration after upgrading Better Auth when its schema changes.
Use createGlobalAuthTables when an app needs extra columns on the auth organization table:
import { createGlobalAuthTables } from '@repo/auth/database'
import { sql } from 'drizzle-orm'
export const { globalOrganizationsTable, globalUsersTable, usersToOrganizationsTable } =
createGlobalAuthTables({
organization: (t) => ({
dataPartnerSharingCode: t
.text('data_partner_sharing_code')
.notNull()
.unique()
.default(sql`upper(substr(replace(gen_random_uuid()::text, '-', ''), 1, 6))`),
}),
})Background session context
Use runWithSession when code runs outside a SvelteKit request, such as queue workers.
import { getSession, runWithSession } from '@repo/auth/server'
await runWithSession(session, async () => {
const session = getSession()
})Pass null to create an explicit no-session context. In that context getSession({ throwError: false }) returns undefined, and getSession() throws a 401 instead of reading SvelteKit request state.
Mobile Auth
Install the Capacitor plugins directly in the mobile app so cap sync can discover them:
pnpm add @capacitor/browser @capacitor/core capacitor-secure-storage-pluginConfigure one client per mobile app:
import { createMobileAuth } from '@repo/auth/mobile'
export const mobileAuth = createMobileAuth({
baseURL: 'https://api.example.com',
clientId: 'example-mobile',
})mobileAuth.start('google') or mobileAuth.start('github') opens that provider in the system browser and stores the resulting bearer token in Keychain or Keystore. The backend verification route must complete the device approval after the provider redirects back. Call mobileAuth.cancel() during logout.
The backend device authorization adapter can import deviceCodesTable from @repo/auth/global.
Access control
Gates
You can easily create gates yourself:
import type { Gate } from '@repo/auth'
export function createAuthorityGate(authorities: string[]): Gate {
return (session) => authorities.every((val) => session?.user?.authorities.includes(val) || false)
}Import built-in gates and gate results from the server entry point in server and remote modules:
import { createGateResult, isAuthenticatedGate, permissionGate } from '@repo/auth/server'Error Code
There is a possibility to change the error code by returning a GateResult.
export function createAuthorityGate(authorities: string[]): Gate {
return (session) => {
const = authorities.every((val) => session?.user?.authorities.includes(val) || false);
if (!authorities) {
return createGateResult(false, 401);
}
return true;
}
}Checking access
We recommend wrapping the validate session per application since the auth logic can differ.
onUnauthenticatedis called when the gate result is 401. This should only be done when there is no useronUnauthorizedis called on a 403. This is the default behaviour.
export async function validateSession(gates?: Gate[]) {
await validateAccessInSvelte({
gates,
onUnauthenticated: () => {
const request = getRequestEvent()
const redirectTarget = encodeURIComponent(request.url.pathname + request.url.search)
redirect(307, `/login?redirect=${redirectTarget}`)
},
onUnauthorized: () => {
redirect(307, '/dashboard')
},
})
}Contributing
Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.
Please make sure to update tests as appropriate.
License
This library is licensed under the MIT License.
Contact
Jessie Liauw A Fong - github