SvelteKit + Drizzle
This is an example of using aponia.js with SvelteKit (opens in a new tab) and Drizzle ORM (opens in a new tab) with the basic schema.
Define Database Schema
src/lib/server/db/schema.ts
import { createId } from '@paralleldrive/cuid2'
import { relations, type InferSelectModel } from 'drizzle-orm'
import { primaryKey, sqliteTable, text } from 'drizzle-orm/sqlite-core'
export const user = sqliteTable('user', {
id: text('id').primaryKey().$defaultFn(createId),
name: text('name'),
})
export const account = sqliteTable(
'account',
{
userId: text('user_id')
.references(() => user.id)
.notNull(),
providerId: text('provider_id').notNull(),
providerAccountId: text('provider_account_id').notNull(),
providerType: text('provider_type'),
},
(table) => {
return {
primaryKey: primaryKey({
columns: [table.userId, table.providerId, table.providerAccountId],
}),
}
},
)
export const session = sqliteTable('session', {
id: text('id').primaryKey().$defaultFn(createId),
userId: text('user_id')
.references(() => user.id, { onDelete: 'cascade' })
.notNull(),
expires: integer('expires').notNull(),
})
export const userRelations = relations(user, (helpers) => {
return {
/**
* A user can have multiple accounts, i.e. ways to login.
*/
account: helpers.many(account),
session: helpers.many(session),
}
})
export const sessionRelations = relations(session, (helpers) => {
return {
/**
* Each session belongs to a single, unique user.
*/
user: helpers.one(user, {
fields: [session.userId],
references: [user.id],
}),
}
})
export const accountRelations = relations(account, (helpers) => {
return {
/**
* Each account belongs to a single, unique user.
*/
user: helpers.one(user, {
fields: [account.userId],
references: [user.id],
}),
}
})
export type Session = typeof session.$inferSelect
export type User = InferSelectModel<typeof user>
export type Account = InferSelectModel<typeof account>
Database Initialization
Initialize a database client.
src/lib/server/db/index.ts
import { createClient } from '@libsql/client/web'
import { drizzle } from 'drizzle-orm/libsql'
import { TURSO_AUTH_TOKEN, TURSO_CONNECTION_URL } from '$env/static/private'
import * as schema from './schema'
export const client = createClient({
url: TURSO_CONNECTION_URL,
authToken: TURSO_AUTH_TOKEN,
})
export const db = drizzle(client, { schema })
export type DbService = typeof db
// Forward all exports from schema.
export * as schema from './schema'
Type definitions
A big part of aponia.js is type safety, and we can augment aponia.js's interfaces with the types defined by the database.
src/app.d.ts
import '@aponia.js/core/types'
import type { Profile } from '@auth/core/types'
import type { RequestEvent } from '@sveltejs/kit'
import type {
Account as DbAccount,
Session as DbSession,
User as DbUser
} from '$server/db'
declare global {
// Augment aponia.js's interfaces.
namespace Aponia {
interface User extends DbUser {}
interface Account extends DbAccount {}
interface Session extends DbSession {}
// When using auth.js, all provider information is normalized
// into simplified "Profile" interface.
interface ProviderAccount extends Profile {}
interface RequestInput {
event: RequestEvent
}
// If the original, raw information is needed, it can be defined and found
// under the mapped account information.
interface ProviderAccountMapping {
github?: GitHubProfile
google?: GoogleProfile
}
}
// Augment SvelteKit's interfaces.
namespace App {
interface Locals {
getUser: () => Promise<Aponia.User | undefined>
}
interface PageData {
user: Aponia.User | undefined
}
}
}
Auth Adapter
After defining the database schema, an aponia.js database adapter should be defined. It should then be converted to an adapter plugin, which allows it to be integrated into the framework lifecycle.
src/lib/server/auth/adapter.ts
import { db, account, session, user } from '../db'
import { AdapterPlugin, type Adapter } from '@aponia.js/core/adapter'
// Raw adapter is a simple object that defines callbacks.
export const rawAdapter: Adapter = {
findAccount: async (_request, response) => {
const foundAccount = await db.query.account.findFirst({
where: and(
eq(account.providerId, response.providerId),
eq(account.providerAccountId, response.providerAccountId),
),
})
return foundAccount
},
getUserFromAccount: async (account, _request, _response) => {
const accountUser = await db.query.user.findFirst({
where: eq(user.id, account.userId),
})
return accountUser
},
createSession: async (user, _account, _request, _response) => {
const [newSession] = await db
.insert(session)
.values({
userId: user.id,
expires: Date.now() + 1000 * 60 * 24,
})
.returning()
return newSession
},
createUser: async (_request, response) => {
const [newUser] = await db
.insert(user)
.values({
name: response.account.name,
avatar: response.account.picture ?? response.account['image'],
})
.returning()
return newUser
},
findUserAccounts: async (user, _request, _response) => {
const userAccounts = await db.query.account.findMany({
where: eq(account.userId, user.id),
})
return userAccounts
},
createAccount: async (user, _request, response) => {
const [newAccount] = await db
.insert(account)
.values({
providerId: response.providerId,
providerAccountId: response.providerAccountId,
userId: user.id,
})
.returning()
return newAccount
},
}
export const adapter = new AdapterPlugin(rawAdapter)
Providers
Define providers to handle specific authentication flows.
Here, we'll define Google and GitHub providers using the auth.js
wrapper.
src/lib/server/auth/providers.ts
import { OAuthProvider } from '@aponia.js/auth.js/providers/oauth'
import { OIDCProvider } from '@aponia.js/auth.js/providers/oidc'
import GitHub from '@auth/core/providers/github'
import Google from '@auth/core/providers/google'
import { GITHUB_ID, GITHUB_SECRET, GOOGLE_ID, GOOGLE_SECRET } from '$env/static/private'
export const github = new OAuthProvider(
GitHub({
clientId: GITHUB_ID,
clientSecret: GITHUB_SECRET,
}),
)
export const google = new OIDCProvider(
Google({
clientId: GOOGLE_ID,
clientSecret: GOOGLE_SECRET,
}),
)
Session
Initialize a session handler.
src/lib/server/auth/session.ts
import { JwtSessionPlugin } from '@aponia.js/core/session/jwt'
export const jwt = new JwtSessionPlugin()
Auth Instance
Prerequisites:
- The database schema has been defined.
- The database connection has been created.
- The adapter plugin has been defined.
- Providers have been defined.
- Session handler has been defined.
Now, create an auth instance.
src/lib/server/auth/index.ts
import { adapter } from './adapter'
import { github, google } from './providers'
import { jwt } from './session'
export const auth = new Auth({
plugins: [github, google, adapter, jwt],
})
SvelteKit Hooks
src/hooks.server.ts
import type { Handle } from '@sveltejs/kit'
import { sequence } from '@sveltejs/kit/hooks'
import { sveltekit } from '@aponia.js/sveltekit'
import { auth } from '$lib/server/auth'
import { jwtSession } from '$lib/server/auth/session'
import { prisma } from '$lib/server/db'
const authHandle = sveltekit(auth)
const sessionHandle: Handle = async ({ event, resolve }) => {
// Cached session, if already attempted to get.
let session: Aponia.Session | false
event.locals.getSession = async () => {
if (session === false) return undefined
const cookieSession = await jwtSession.getSession(event.cookies)
const isValid = cookieSession.expires > Date.now()
if (!isValid) {
session = false
return undefined
}
// Verify that the session in the cookie actually exists in the database.
const dbSession = await prisma.findUnique({
where: {
id: cookieSession.id
}
})
if (dbSession == null) {
session = false
return undefined
}
session = dbSession
return session
}
}
export const handle = sequence(authHandle, sessionHandle)