MonoStack

@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/auth

Authentication 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-plugin

Configure 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.

  • onUnauthenticated is called when the gate result is 401. This should only be done when there is no user
  • onUnauthorized is 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

On this page