Authentication & Authorization
Session management, 2FA, access control, and multi-tenancy.
Package: @repo/auth Built on: Better Auth
Session Management
Sessions flow from locals.session → data.session via layout files.
In Svelte components, access via page data:
<script lang="ts">
import { page } from '$app/state'
const session = page.data.session
const user = session?.user
</script>
{#if user}
<p>Welcome, {user.name}</p>
{:else}
<a href="/login">Sign in</a>
{/if}
Or via props when passed from layout:
<script lang="ts">
const { data } = $props()
const user = data.session?.user
</script>
In remote files (server-side):
// *.remote.ts
import { getUserSession } from '@repo/auth/server'
export const getProfile = query(async () => {
const session = getUserSession()
if (!session) throw new Error('Unauthorized')
return db.select().from(users).where(eq(users.id, session.user.id))
})
Two-Factor Authentication (2FA)
TOTP-based 2FA is included in the boilerplate. See the security settings page for the implementation.
Gate-Based Access Control
Gates are functions that take a session and return a boolean. Use them for authorization checks.
Permission Gates
Use createPermissionGate for role/permission-based checks:
// domains/users/utils/gates.ts
import { createPermissionGate } from '$lib/library/authHelpers'
export function userDeleteGates(id: string) {
return [
createPermissionGate('users', ['delete']),
]
}
Custom Gates
Create custom gates for complex logic:
// domains/organizations/utils/gates.ts
import type { Gate, Session } from '@repo/auth'
import { createPermissionGate } from '$lib/library/authHelpers'
export function userInOrganizationGate(organizationId: string): Gate {
return (session: Session | null) =>
session?.user?.organizations?.some(org => org.id === organizationId) || false
}
export function organizationDeleteGates(id: string) {
return [
createPermissionGate('organizations', ['delete']),
userInOrganizationGate(id),
]
}
Check Permissions in Components
<script lang="ts">
import { page } from '$app/state'
import { hasAccess } from '@repo/auth'
import { organizationDeleteGates } from '$lib/domains/organizations/utils/gates'
const { params } = $props()
const canDelete = await hasAccess(page.data.session, organizationDeleteGates(params.id))
</script>
{#if canDelete}
<button class="text-red-500">Delete</button>
{/if}
Server-Side Authorization
Use validateSession with gates in remote files:
// organizations.remote.ts
import { form } from '$app/server'
import { validateSession } from '$lib/library/authHelpers.server'
import { organizationDeleteGates } from '$lib/domains/organizations/utils/gates'
export const deleteOrganization = form(schema, async ({ id }) => {
await validateSession(organizationDeleteGates(id))
// User is authorized, proceed with deletion
return db.delete(organizations).where(eq(organizations.id, id))
})
Multi-Tenant Support
MonoStack supports multi-tenant applications with separate schemas per tenant.
Tenant-Scoped Queries
Use getTenantDb from your app's managed database to get a db connection scoped to the current organization:
// *.remote.ts
import { getTenantDb } from '$lib/db/managedDb.server'
export const getProducts = query(async () => {
const { db } = await getTenantDb()
return db.select().from(products)
})
Organization Switching
See the boilerplate's +layout.svelte for organization switching implementation.
Invitations
Invitations are managed via the /invitations route. See the boilerplate for the full implementation.
Permissions
Use PermissionsField for editing role permissions:
// domains/roles/utils/schemas.ts
import { PermissionsField } from '@repo/auth'
import { authDomains } from '$lib/library/domainActions'
import { zaf } from '@repo/form'
export const roleInsertSchema = createInsertSchema(rolesTable)
.pick({ name: true })
.extend({
permissions: zaf(z.array(permissionInsertSchema).default([]), {
component: PermissionsField,
props: {
domains: authDomains,
},
}),
})
Full Documentation
See @repo/auth README for complete API reference.