"how do I create user accounts with sveltekit?"
15.3s
Add to Favorites
Cozy Upvote
Share
Export

Creating User Accounts in a SvelteKit App

SvelteKit doesn’t ship with built‑in authentication, so you’ll add it yourself or use a service. Below are three common approaches, each with a complete “starter kit” you can copy into a fresh SvelteKit project.

Approach When to use it Main benefits
Lucia Auth (lightweight, fully controllable) You want full control over the DB schema, sessions, and password hashing. Small bundle size, first‑class SvelteKit middleware, works with any ORM (Prisma, Drizzle, etc.)
Supabase (hosted BaaS) You prefer an out‑of‑the‑box backend (Postgres + auth + storage) and don’t want to manage a DB yourself. Instant email/password, social logins, magic‑link, real‑time, no server code required
DIY (custom from scratch) You have very specific requirements (e.g., custom OTP flow) or want to learn the internals. Total freedom, can pick any hashing library, DB, session store

1️⃣ Lucia Auth (recommended for most SvelteKit projects)

1. Install dependencies

npm i lucia @lucia-auth/adapter-prisma prisma @prisma/client bcrypt

If you use a different ORM, swap the Prisma adapter for the one that matches your DB5.

2. Set up Prisma (or your DB)

Create a prisma/schema.prisma file with at least User and Session tables (Lucia will add the required fields). Then run:

npx prisma migrate dev --name init

This creates the tables and a prisma client you can import.

3. Configure Lucia (src/lib/server/lucia.ts)

import { lucia } from 'lucia';
import { sveltekit } from 'lucia/middleware';
import { PrismaAdapter } from '@lucia-auth/adapter-prisma';
import { PrismaClient } from '@prisma/client';
import bcrypt from 'bcrypt';

const prisma = new PrismaClient();

export const auth = lucia({
  adapter: new PrismaAdapter(prisma),
  env: process.env.NODE_ENV === 'development' ? 'DEV' : 'PROD',
  middleware: sveltekit(),
  sessionCookie: {
    attributes: {
      secure: process.env.NODE_ENV === 'production'
    }
  },
  getUserAttributes: (data) => ({
    email: data.email,
    // add any extra fields you store on the user
  })
});

export type Auth = typeof auth;

4. Sign‑up page (src/routes/signup/+page.svelte)

<script lang="ts">
  import { enhance } from '$app/forms';
</script>

<form method="POST" use:enhance>
  <label>
    Email
    <input name="email" type="email" required />
  </label>
  <label>
    Password
    <input name="password" type="password" minlength="8" required />
  </label>
  <button type="submit">Create account</button>
</form>

5. Sign‑up action (src/routes/signup/+page.server.ts)

import type { Actions } from './$types';
import { auth } from '$lib/server/lucia';
import { PrismaClient } from '@prisma/client';
import bcrypt from 'bcrypt';

const prisma = new PrismaClient();

export const actions: Actions = {
  default: async ({ request }) => {
    const form = await request.formData();
    const email = String(form.get('email') ?? '').toLowerCase();
    const password = String(form.get('password') ?? '');

    // ---- basic validation ----
    if (!email || !password || password.length < 8) {
      return { error: 'Invalid input' };
    }

    // ---- prevent duplicate accounts ----
    const existing = await prisma.user.findUnique({ where: { email } });
    if (existing) {
      return { error: 'User already exists' };
    }

    // ---- hash password ----
    const passwordHash = await bcrypt.hash(password, 12);

    // ---- create user & session with Lucia ----
    const user = await auth.createUser({
      key: {
        providerId: 'email',
        providerUserId: email,
        password: passwordHash
      },
      attributes: { email }
    });

    // optional: automatically log the user in
    const session = await auth.createSession(user.userId);
    // Lucia will set the session cookie for you via the middleware

    return { success: true };
  }
};

6. Protecting routes (example src/routes/dashboard/+page.server.ts)

import { auth } from '$lib/server/lucia';
import type { PageServerLoad } from './$types';

export const load: PageServerLoad = async (event) => {
  const { user } = await auth.validateUser(event.locals);
  if (!user) {
    // redirect to login if not authenticated
    throw redirect(302, '/login');
  }
  return { user };
};

That’s all you need for a fully functional username/password flow with sessions stored in your DB.


2️⃣ Supabase (hosted backend)

1. Install the Supabase client

npm i @supabase/supabase-js

2. Initialise the client (src/lib/supabase.ts)

import { createClient } from '@supabase/supabase-js';

export const supabase = createClient(
  import.meta.env.VITE_SUPABASE_URL,
  import.meta.env.VITE_SUPABASE_ANON_KEY
);

Add the two environment variables to your .env (or Vite env) file.

3. Sign‑up page (same markup as the Lucia example)

4. Sign‑up action (src/routes/signup/+page.server.ts)

import type { Actions } from './$types';
import { supabase } from '$lib/supabase';
import { fail } from '@sveltejs/kit';

export const actions: Actions = {
  default: async ({ request }) => {
    const form = await request.formData();
    const email = String(form.get('email') ?? '').toLowerCase();
    const password = String(form.get('password') ?? '');

    if (!email || !password) {
      return fail(400, { error: 'Invalid input' });
    }

    const { error } = await supabase.auth.signUp({ email, password });
    if (error) {
      return fail(400, { error: error.message });
    }

    // Supabase automatically sends a confirmation email.
    return { success: true };
  }
};

5. Session handling

Supabase stores a JWT in localStorage and provides a helper $app/stores to keep the auth state reactive. In a layout you can subscribe to the auth store:

// src/routes/+layout.svelte
<script lang="ts">
  import { supabase } from '$lib/supabase';
  import { onMount } from 'svelte';
  import { page } from '$app/stores';

  onMount(() => {
    supabase.auth.onAuthStateChange((_event, session) => {
      // you can set a cookie, redirect, etc.
    });
  });
</script>

<slot />

Supabase also supports social providers, magic links, and row‑level security out of the box 4.


3️⃣ DIY Authentication (from scratch)

If you prefer to write every piece yourself, the pattern is essentially:

  1. Receive a POSTed form (email/username + password).
  2. Validate & hash the password (e.g., using argon2 or bcrypt).
  3. Store the user record in your DB.
  4. Create a session identifier (random token or JWT).
  5. Set a cookie (Set-Cookie header) that the client will send on subsequent requests.
  6. Validate the cookie on protected endpoints.

Example using Argon2 and Prisma

Install hashing library

npm i argon2

Server‑side action (src/routes/signup/+page.server.ts)

import type { Actions } from './$types';
import { prisma } from '$lib/prisma'; // your Prisma client
import { hash } from 'argon2';
import { fail, redirect } from '@sveltejs/kit';
import { v4 as uuid } from 'uuid';

export const actions: Actions = {
  default: async ({ request, cookies }) => {
    const form = await request.formData();
    const email = String(form.get('email') ?? '').toLowerCase();
    const password = String(form.get('password') ?? '');

    if (!email || !password) return fail(400, { error: 'Invalid input' });

    // check duplicate
    const exists = await prisma.user.findUnique({ where: { email } });
    if (exists) return fail(400, { error: 'User already exists' });

    const passwordHash = await hash(password);
    const user = await prisma.user.create({
      data: { email, passwordHash }
    });

    // ---- create a simple session record ----
    const sessionId = uuid();
    await prisma.session.create({
      data: {
        id: sessionId,
        userId: user.id,
        // optional: expiresAt, userAgent, etc.
      }
    });

    // ---- set http‑only cookie ----
    cookies.set('session_id', sessionId, {
      path: '/',
      httpOnly: true,
      sameSite: 'lax',
      secure: process.env.NODE_ENV === 'production',
      maxAge: 60 * 60 * 24 * 30 // 30 days
    });

    // redirect to a protected page
    throw redirect(302, '/dashboard');
  }
};

Protecting a route (src/routes/dashboard/+page.server.ts)

import type { PageServerLoad } from './$types';
import { prisma } from '$lib/prisma';
import { redirect } from '@sveltejs/kit';

export const load: PageServerLoad = async ({ cookies }) => {
  const sessionId = cookies.get('session_id');
  if (!sessionId) throw redirect(302, '/login');

  const session = await prisma.session.findUnique({
    where: { id: sessionId },
    include: { user: true }
  });

  if (!session) throw redirect(302, '/login');

  return { user: session.user };
};

Tips for a production‑ready DIY setup

Concern Recommendation
Password hashing Use Argon2 with high memory cost or bcrypt ≥ 12 rounds.
Session storage Store sessions in a DB table with an expiresAt column, or use signed JWTs (@sveltejs/kit can sign cookies).
CSRF SvelteKit’s built‑in enhance automatically sends a CSRF token; keep method="POST" for state‑changing actions.
Rate limiting Add a middleware (e.g., express-rate-limit via @sveltejs/adapter-node) to throttle signup attempts.
Email verification Send a one‑time token (via nodemailer or a service like Resend) and store it in a verificationTokens table.

4️⃣ Choosing the Right Approach

Feature Lucia Supabase DIY
Control over DB schema ❌ (managed)
Zero‑backend hosting ❌ (you still need a DB)
Social login out of the box ✅ (via adapters) ❌ (you must implement)
Learning curve Moderate (middleware & adapters) Low (just the client) High (you write everything)
Cost Depends on your DB host Free tier + paid storage Depends on your infra

If you’re starting a new app and want a solid, SvelteKit‑first solution, Lucia Auth is the sweet spot. For prototypes or when you want a fully managed backend, go with Supabase. Use the DIY route only when you have very custom requirements.


All code snippets assume a recent SvelteKit version (≥ 1.20) and TypeScript support.

No follow-up threads yet

Dive deeper into this topic with a follow-up.

Sign in to start a follow-up thread