หลัง Guide ก่อนหน้าบน Upstash Blog ได้ขึ้น Bytes Newsletter ผมคิดว่าเล่น SvelteKit ต่อไปดี
身為 Svelte Super Fan ผมเห็นคนกระโดดขึ้นมามากขึ้นทุกวัน ทำให้ตื่นเต้นกับอนาคตมาก
เครื่องมือที่ยัง Under Radar หนึ่งคือ Lucia
Guide นี้จะแสดงวิธี Setup Authentication ด้วย Lucia ใช้ PlanetScale สำหรับ Database และ Upstash Redis จัดการ Sessions
Screenshot เป้าหมายสุดท้าย:

Guide นี้เป็น SvelteKit แต่ Lucia รองรับ Framework ไหนก็ได้ ส่วนใหญ่ Apply ได้หมด
What is Lucia? - Lucia คืออะไร?
Simple: Lucia คือ Library TypeScript ทำให้จัดการ Users และ Sessions ง่ายขึ้น ต้นทางสร้างสำหรับ SvelteKit แต่ Evolve จนรองรับ Framework เกือบทุกตัว
จุดเด่น Lucia: ให้ Primitives ครบครันจัดการ Complexity ของ Auth โดยไม่犧牲 UX คุณตัดสินใจ Structure Code และ Handle UX เอง Lucia มี Key Parts:
Middleware ให้ Lucia อ่าน Request/Response สำหรับ Framework/Runtime ต่างๆ
ตัวอย่าง Config Middleware:
import { lucia } from "lucia";
import { node } from "lucia/middleware";
// import { nextjs } from "lucia/middleware";
// import { h3 } from "lucia/middleware";
export const auth = lucia({
env: "DEV", // "PROD" ถ้า Deploy HTTPS
middleware: node(),
});
Database Adapters ให้ Lucia Store/Retrieve Users และ Sessions มี 2 ประเภท: Regular Adapters และ Session Adapters Guide นี้ใช้ MySQL บน PlanetScale เก็บ Users และ Redis บน Upstash จัดการ Sessions
ตัวอย่าง Config Database Adapter:
import { prisma } from "@lucia-auth/adapter-prisma";
import { PrismaClient } from "@prisma/client";
import { lucia } from "lucia";
const client = new PrismaClient();
const auth = lucia({
env: "DEV",
adapter: prisma(client),
});
Prerequisites - เงื่อนไขก่อนเริ่ม
- เข้าใจ SvelteKit พื้นฐาน Forms และ Routing เป็นประโยชน์
- คุ้นเคย Drizzle ORM พื้นฐาน
- Account และ Database บน PlanetScale
- เข้าถึง Redis Instance เช่น Upstash Redis
Getting Started - เริ่มต้น
เพื่อประสิทธิภาพ ไม่สร้าง App จากศูนย์ Clone Directory sveltekit-lucia-redis จาก Upstash Examples Repo
หลัง Download cd เข้า App Install Dependencies และ Set .env โดย Duplicate .env.example
Understanding the Key Parts - เข้าใจส่วนสำคัญ
src/lib/server/auth/index.ts- Config Luciasrc/lib/server/drizzle- Drizzle สร้าง MySQL Schema Push ไป PlanetScale ด้วย Drizzle Kitsrc/lib/server/planetscale- Export Upstash Client ใช้ใน Lucia Adapter Config จัดการ Userssrc/lib/server/upstash- Export Upstash Client ใช้ใน Lucia Adapter Config จัดการ Sessions
Breaking Down the Code - แยกวิเคราะห์โค้ด
Configuring Lucia - กำหนดค่า Lucia
สร้างไฟล์ src/lib/server/auth/index.ts:
import { planetscale } from "@lucia-auth/adapter-mysql";
import { dev } from "$app/environment";
import { lucia } from "lucia";
import { sveltekit } from "lucia/middleware";
import { ps } from "../planetscale";
export enum PROVIDER_ID { EMAIL = "email", }
export const auth = lucia({
adapter: {
user: planetscale(ps, {
user: "users",
key: "keys",
session: null, // Sessions จัดการโดย Upstash Redis
}),
},
middleware: sveltekit(),
env: dev ? "DEV" : "PROD",
getUserAttributes: (data) => {
return { userId: data.id, email: data.email };
},
});
export type Auth = typeof auth;
สิ่งที่เกิดขึ้น:
Import lucia Function Setup Config Lucia
Config adapter Property บอก Lucia จัดการ Users/Sessions ยังไง
Set session เป็น null เพราะใช้ Redis จัดการ Sessions หากใช้ String 'session' Lucia จะใช้ Adapter เดียวกันทั้ง Users/Sessions (Strings ตรงกับ Tables ใน Database)
Session Adapter รอ會 später
middleware Property บอก Lucia ใช้ SvelteKit อ่าน Request/Response Objects
สังเกต Exported Auth Type เป็น Type ของ auth Object จำเป็นสำหรับ Setup SvelteKit Locals
Getting Great Type Inference with Lucia - Type Inference ที่ดี
Lucia เขียน TypeScript Type Inference ดี Out-of-the-box ให้ SvelteKit รู้ Auth Type ที่สร้าง
เปิด app.d.ts เพิ่ม:
import type { Auth as LuciaAuth } from "$lib/server/auth";
import type { AuthRequest, Session, User } from "lucia";
declare global {
namespace App {
interface Locals {
auth: AuthRequest;
session: Session | null;
}
interface PageData {
user?: User;
}
}
}
/// <reference types="lucia" />
declare global {
namespace Lucia {
type Auth = LuciaAuth;
type DatabaseUserAttributes = { email: string; };
type DatabaseSessionAttributes = {};
}
}
export {};
เพิ่ม Auth Type ใน Lucia Namespace เข้าถึง auth Object จาก locals ใน SvelteKit Routes ได้ ทุกอย่าง Import จาก Lucia จะมี Types ถูกต้อง
Setup hooks.server.ts Bind AuthRequest และ Session Object กับ Request ปัจจุบัน เข้าถึงง่ายผ่าน locals:
import type { Handle } from "@sveltejs/kit";
import { sequence } from "@sveltejs/kit/hooks";
import { auth } from "$lib/server";
const auth_handle: Handle = async ({ event, resolve }) => {
event.locals.auth = auth.handleRequest(event);
event.locals.session = await event.locals.auth.validate();
return resolve(event);
};
export const handle = sequence(auth_handle);
Import sequence Helper รัน Multiple Hooks เรียงลำดับ จะใช้ตอน Protect Routes
Creating the User Model - สร้าง User Model
Lucia Configured แล้ว สร้าง User Model ใช้ Drizzle ORM (Hot Right Now)

Meme ของพวกเขาจุดเปลี่ยน ดูตัวนี้
ก่อนต่อ ต้องสร้าง Database บน PlanetScale และ Setup Drizzle Config File ช่วย Drizzle CLI Connect Database:
import dotenv from "dotenv";
import type { Config } from "drizzle-kit";
dotenv.process();
const username = process.env.DATABASE_USERNAME;
const password = process.env.DATABASE_PASSWORD;
const host = process.env.DATABASE_HOST;
const db = process.env.DATABASE_NAME;
const connectionString = `mysql://${username}:${password}@${host}/${db}?ssl={"rejectUnauthorized":true}`;
export default {
schema: "./src/lib/server/drizzle/schema/index.ts",
driver: "mysql2",
dbCredentials: { connectionString: connectionString },
} satisfies Config;
ใช้ MySQL Adapter Lucia คาดหวัง User Model Structure เฉพาะ รายละเอียดใน Docs ใส่ใน src/lib/server/drizzle/schema/index.ts:
import { relations } from "drizzle-orm";
import { bigint, datetime, index, int, mysqlEnum, mysqlTable, timestamp, unique, varchar } from "drizzle-orm/mysql-core";
export const users = mysqlTable(
"users",
{
id: varchar("id", { length: 255 }).primaryKey(),
createdAt: timestamp("createdAt").defaultNow().onUpdateNow().notNull(),
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
email: varchar("email", { length: 191 }).notNull(),
},
(table) => {
return {
idIdx: index("users_id_idx").on(table.id),
userIdKey: unique("users_id_key").on(table.id),
};
},
);
export const keys = mysqlTable(
"keys",
{
id: varchar("id", { length: 255 }).primaryKey(),
hashedPassword: varchar("hashed_password", { length: 255 }),
userId: varchar("user_id", { length: 255 }).notNull(),
},
(table) => {
return {
userIdIdx: index("keys_user_id_idx").on(table.userId),
keyIdKey: unique("keys_id_key").on(table.id),
};
},
);
รัน pnpm drizzle-kit push:mysql Push Schema ไป PlanetScale
Voila! User Model พร้อมให้ Lucia จัดการ Users
Setting Up Session Management - ตั้งค่า Session Management
User Model พร้อม ตั้งค่า Session Management ใช้ Upstash Redis จัดการ Sessions สมัคร Free Account ได้ที่นี่

ใน Dashboard สร้าง Database ใหม่ Copy Env Variables

เพิ่ม Variables ใน .env และเพิ่มใน src/lib/server/upstash/index.ts:
import { Redis } from "@upstash/redis";
import { UPSTASH_REDIS_REST_TOKEN, UPSTASH_REDIS_REST_URL } from "$env/static/private";
export const upstashClient = new Redis({ url: UPSTASH_REDIS_REST_URL, token: UPSTASH_REDIS_REST_TOKEN });
จำได้ไหมตอน Config Lucia Set session เป็น null เพราะใช้ Redis จัดการ Sessions Config ตอนนี้:
import { planetscale } from "@lucia-auth/adapter-mysql";
import { upstash } from "@lucia-auth/adapter-session-redis";
import { dev } from "$app/environment";
import { lucia } from "lucia";
import { sveltekit } from "lucia/middleware";
import { ps } from "../planetscale";
import { upstashClient } from "../upstash";
export enum PROVIDER_ID { EMAIL = "email", }
export const auth = lucia({
adapter: {
user: planetscale(ps, { user: "users", key: "keys", session: null }),
session: upstash(upstashClient), // ใช้ Upstash Redis สำหรับ Sessions
},
middleware: sveltekit(),
env: dev ? "DEV" : "PROD",
getUserAttributes: (data) => { return { userId: data.id, email: data.email }; },
});
export type Auth = typeof auth;
โชคดี Lucia มี Upstash Redis Adapter Built-in Import และส่ง Upstash Client เข้าไปง่ายมาก!
Creating the Routes - สร้าง Routes
Lucia Configured แล้ว สร้าง Routes สร้างโฟลเดอร์ใน src/routes:
src/routes/auth→signin,signupsrc/routes/app
Tip: Group Features ใต้ Folder เดียวกัน จัดการ Redirects และ Protect Routes ง่าย
Creating the Signin Page - สร้างหน้า Sign In
สร้าง src/routes/auth/signin/+page.svelte (ข้าม Styling เน้น Functionality Full Code ใน Repo):
<script lang="ts">
import { enhance } from '$app/forms';
import { Button, Input, Label, PasswordInput } from '$lib/components/common';
import type { ActionData } from './$types';
export let form: ActionData;
let loading = false;
let email = '';
let password = '';
</script>
<form method="POST" use:enhance={() => { loading = true; return async ({ update }) => { loading = false; update(); }; }} >
{#if form && form.error}
<div class="p-2 mb-4 text-sm text-center text-red-900 bg-red-200 rounded-sm"> Error: {form.error} </div>
{/if}
<div class="grid gap-2.5">
<div class="grid gap-1">
<Label for="email">Email</Label>
<Input bind:value={email} name="email" placeholder="Email" type="email" autoCapitalize="none" autoComplete="email" autoCorrect="off" required={true} />
</div>
<div class="grid gap-1">
<Label for="password">Password</Label>
<PasswordInput name="password" placeholder="Password" bind:value={password} />
</div>
<div class="mt-6 col-span-full">
<Button type="submit" class="w-full" disabled={loading}> {#if loading} Loading... {:else} Sign in {/if} </Button>
</div>
</div>
</form>
use:enhance Progressive Enhance Form แสดง Loading State Submit Form
Handling the Signin Request - จัดการ Request Sign In
SvelteKit จัดการ POST Requests ง่าย สร้าง +page.server.ts Directory เดียวกับ +page.svelte Export actions Object มี default Property:
import { fail, redirect } from "@sveltejs/kit";
import { auth, PROVIDER_ID } from "$lib/server";
import { LuciaError } from "lucia";
import type { Actions, PageServerLoad } from "./$types";
export const actions = { /* actions here */ };
Import PROVIDER_ID Enum และ auth จาก src/lib/server/auth มี Methods จัดการ Users/Sessions
ดู actions Object ดึง Form Data จาก Request Housekeeping:
actions = {
default: async ({ request, locals }) => {
const formData = await request.formData();
const email = formData.get("email") as string;
const password = formData.get("password") as string;
const fields = [email, password];
if (fields.some(field => !field)) {
return fail(400, { error: "All fields are required" });
}
พยายาม Sign In User หากไม่มี User หรือ Password ผิด Lucia Throw Error Catch Error Return 400 พร้อม Error Message:
try {
const user = await auth.useKey(PROVIDER_ID.EMAIL, email.toLowerCase(), password);
const session = await auth.createSession({ userId: user.userId, attributes: {} });
locals.auth.setSession(session);
} catch (err) {
if (err instanceof LuciaError && (err.message === 'AUTH_INVALID_KEY_ID' || err.message === 'AUTH_INVALID_PASSWORD')) {
return fail(400, { error: 'Incorrect username of password' });
}
return fail(400, { error: 'An unknown error occurred' });
}
User Exists + Password Correct → Create New Session
Redirect ไป Dashboard:
return redirect('/app');
Lucia Abstract ความซับซ้อนของ Auth เรียก Methods ให้ถูกต้อง Lucia จัดการหมด Hash Passwords, Create Sessions, Manage Cookies Type-safe!

Creating the Signup Page - สร้างหน้า Sign Up
สร้าง src/routes/auth/signup/+page.svelte คล้าย Signin แต่ขอ Password Confirmation
<script lang="ts">
import { enhance } from '$app/forms';
import { Button, Input, Label, PasswordInput } from '$lib/components/common';
import type { ActionData } from './$types';
export let form: ActionData;
let loading = false;
let email = '';
let password = '';
let passwordConfirmation = '';
</script>
<form method="POST" use:enhance={() => { loading = true; return async ({ update }) => { loading = false; update(); }; }} >
{#if form && form.error} <div class="p-2 mb-4 text-sm text-center text-red-900 bg-red-200 rounded-sm"> Error: {form.error} </div> {/if}
<div class="grid gap-2.5">
<div class="grid gap-1"> <Label for="email">Email</Label> <Input bind:value={email} name="email" placeholder="Email" type="email" autoCapitalize="none" autoComplete="email" autoCorrect="off" required={true} /> </div>
<div class="grid gap-1"> <Label for="password">Password</Label> <PasswordInput name="password" placeholder="Password" bind:value={password} /> </div>
<div class="grid gap-1"> <Label for="passwordConfirmation">Confirm password</Label> <PasswordInput name="passwordConfirmation" placeholder="Repeat password" bind:value={passwordConfirmation} /> </div>
<div class="mt-6 col-span-full"> <Button type="submit" class="w-full" disabled={loading}> {#if loading} Loading... {:else} Sign in {/if} </Button> </div>
</div>
</form>
Handling the Signup Request - จัดการ Request Sign Up
สร้าง src/routes/auth/signup/+page.server.ts Pattern คล้าย Signup แต่ต่างกันที่ Data Retrieval, Validation, Create User, Handle Existing Users
Imports เหมือน Signin ข้าม
ดึง Form Data Housekeeping Check Passwords Match:
const formData = await request.formData();
const email = formData.get('email') as string;
const password = formData.get('password') as string;
const passwordConfirmation = formData.get('passwordConfirmation') as string;
const fields = [email, password, passwordConfirmation];
if (fields.some((field) => !field)) { return fail(400, { error: 'All fields are required' }); }
if (password !== passwordConfirmation) { return fail(400, { error: 'Passwords do not match' }); }
หา User โดย Email ด้วย Drizzle ORM หากมี User Return 400 Error:
try {
const user = await db.query.users.findFirst({ where: eq(schema.users.email, email.toLowerCase()) });
if (user) { return fail(400, { error: "User with this email already exists" }); }
ไม่มี User → Create User ใหม่ด้วย Lucia:
const newUser = await auth.createUser({
key: { providerId: PROVIDER_ID.EMAIL, providerUserId: email.toLowerCase(), password: password },
attributes: { email },
});
Create Session Redirect Dashboard:
const session = await auth.createSession({ userId: newUser.userId, attributes: {} });
locals.auth.setSession(session);
Easy Peasy!
Bonus: Creating the Signout Page - สร้างหน้า Sign Out
สร้าง Endpoint Sign Out src/routes/auth/signout/+server.ts:
import { auth } from "$lib/server";
import type { RequestHandler } from "./$types";
export const POST: RequestHandler = async ({ locals }) => {
const session = await locals.auth.validate();
if (!session) { return new Response(null, { status: 400 }); }
await auth.invalidateAllUserSessions(session.userId);
await auth.invalidateSession(session.sessionId);
locals.auth.setSession(null);
return new Response(null, { status: 200 });
};
Lucia ทำ Invalidate Sessions ง่าย เรียก Endpoint ตอน User กด Sign Out
<script lang="ts">
import { goto } from '$app/navigation';
import { Button } from '$lib/components/common';
async function handleSignOut() {
const response = await fetch('/auth/signout', { method: 'POST', headers: { 'Content-Type': 'application/json' } });
if (response.ok) { goto('/auth/signin', { replaceState: true, invalidateAll: true }); }
}
</script>
<Button on:click={handleSignOut}>Sign out</Button>
That's a Wrap - จบแล้ว
สร้าง Type-safe Authentication ด้วย Lucia, PlanetScale, Upstash Redis สำเร็จ เพียงแค่ผิวหน้าของความสามารถ Lucia
Repo สำหรับ Guide ที่นี่ อยากเรียนรู้ Lucia เพิ่มเติม อ่าน Docs แนะนำ
มาร่วม Upstash Discord Community กำลังสนุกอยู่ อยาก SvelteKit Content เพิ่ม หาได้ที่ Blog ของผม