There are several ways to add authentication to your Val Town app. Pick the approach that fits your needs, then use the provided prompt to have Townie or your LLM implement it.
| Approach | Best for | Users need |
|---|---|---|
| Sign in with Val Town | Apps for the Val Town community | A Val Town account |
| HTTP Basic Auth | Quick password protection | A shared password |
| Cookie-based login page | Personal projects, admin dashboards | A shared password |
| Magic link auth | Multi-user apps with email login | An email address |
| Third-party providers | Production apps, social login, SSO | Depends on provider |
Sign in with Val Town
Section titled “Sign in with Val Town”The std/oauth middleware lets users sign in with their Val Town account. It handles the full OAuth 2.0 flow, stores sessions in encrypted cookies (no database needed), and gives you access to the user’s Val Town profile.
The middleware automatically handles /auth/login, /auth/callback, and /auth/logout routes. Sessions last 30 days.
Reference implementation:
import { Hono } from "npm:hono";import { getOAuthUserData, oauthMiddleware,} from "https://esm.town/v/std/oauth/middleware.ts";
const app = new Hono();
app.get("/", async (c) => { const session = await getOAuthUserData(c.req.raw); if (session?.user) { return c.html( `<p>Logged in as ${session.user.username}</p> <a href="/auth/logout">Log out</a>`, ); } return c.html(`<a href="/auth/login">Log in with Val Town</a>`);});
export default oauthMiddleware(app.fetch);HTTP Basic Auth
Section titled “HTTP Basic Auth”The simplest way to password-protect your val. The browser shows a built-in login prompt — no UI to build.
Store your credentials in environment variables (BASIC_AUTH_USER and BASIC_AUTH_PASS), then check the Authorization header:
export default async function (req: Request): Promise<Response> { const auth = req.headers.get("Authorization");
if (auth) { const [, encoded] = auth.split(" "); const decoded = atob(encoded); const [user, pass] = decoded.split(":");
if ( user === Deno.env.get("BASIC_AUTH_USER") && pass === Deno.env.get("BASIC_AUTH_PASS") ) { return new Response("Welcome! You're authenticated."); } }
return new Response("Unauthorized", { status: 401, headers: { "WWW-Authenticate": 'Basic realm="Protected"' }, });}Cookie-based Auth with a Login Page
Section titled “Cookie-based Auth with a Login Page”If you want a custom login form instead of the browser’s basic auth popup, you can use a cookie to persist the session. Good for personal projects and admin dashboards.
Store your password in an environment variable (APP_PASSWORD). Reference implementation:
const PASSWORD = Deno.env.get("APP_PASSWORD");const COOKIE_NAME = "session";
export default async function (req: Request): Promise<Response> { const url = new URL(req.url); const cookies = parseCookies(req.headers.get("Cookie") || "");
// Handle login form submission if (url.pathname === "/login" && req.method === "POST") { const form = await req.formData(); if (form.get("password") === PASSWORD) { return new Response(null, { status: 302, headers: { Location: "/", "Set-Cookie": `${COOKIE_NAME}=${PASSWORD}; Path=/; HttpOnly; SameSite=Lax`, }, }); } return loginPage("Wrong password."); }
// Handle logout if (url.pathname === "/logout") { return new Response(null, { status: 302, headers: { Location: "/login", "Set-Cookie": `${COOKIE_NAME}=; Path=/; Max-Age=0`, }, }); }
// Show login page if not authenticated if (cookies[COOKIE_NAME] !== PASSWORD) { return loginPage(); }
// Authenticated — serve your app return new Response( "Welcome! You're logged in. <a href='/logout'>Log out</a>", { headers: { "Content-Type": "text/html" }, }, );}
function loginPage(error?: string) { return new Response( `<form method="POST" action="/login"> ${error ? `<p style="color:red">${error}</p>` : ""} <input type="password" name="password" placeholder="Password" /> <button type="submit">Log in</button> </form>`, { headers: { "Content-Type": "text/html" }, status: 401 }, );}
function parseCookies(cookie: string): Record<string, string> { return Object.fromEntries( cookie.split(";").map((c) => c .trim() .split("=") .map((s) => s.trim()), ), );}Magic Link Auth with Lucia
Section titled “Magic Link Auth with Lucia”For real multi-user auth where each user signs in with their email, remix the Magic Link Starter template. It uses Lucia for session management, Val Town SQLite for storing users and sessions, and Val Town Email for sending magic links.
The flow:
- User enters their email on the login page
- Your val generates a token and emails them a magic link
- User clicks the link, the token is verified, and a session cookie is set
- Sessions last 30 days with automatic token rotation
Other Auth Providers and Libraries
Section titled “Other Auth Providers and Libraries”You can also use any third-party auth provider by connecting to their APIs from your val. Tell your LLM which provider you want to use and it can integrate it.
Open-source libraries
Section titled “Open-source libraries”- Better Auth — Comprehensive TypeScript auth framework with built-in support for email/password, OAuth, two-factor, and more
- Auth.js — Framework-agnostic auth (evolved from NextAuth.js) with 80+ OAuth providers
- Lucia — Lightweight, session-based auth library that works with any database
- Arctic — Minimal OAuth 2.0 client library with built-in providers (Google, GitHub, Discord, etc.) — pairs well with Lucia
- Oslo — Low-level utilities for sessions, tokens, cookies, and crypto from the Lucia team
Hosted auth providers
Section titled “Hosted auth providers”- Clerk — Drop-in UI components and APIs for sign-up, sign-in, and user management
- Kinde — Auth, feature flags, and billing in one platform
- WorkOS — Enterprise SSO and directory sync via API
- Supabase Auth — Auth bundled with Supabase’s database, works via their JS client
- Auth0 — Flexible identity platform with social and enterprise login
- Firebase Auth — Google’s auth service with email, phone, and social sign-in
Legacy Val Town auth middleware
Section titled “Legacy Val Town auth middleware”These older Val Town community libraries still work but are no longer actively recommended. Use std/oauth or the Magic Link Starter instead.
stevekrouse/lastlogin— Auth via LastLogin.io (email, Google, GitHub) with no API keys. Not security-audited; best for demos and prototypes.stevekrouse/lucia_middleware— Username/password auth middleware using Lucia and Val Town SQLite. Includes built-in signup/login pages but limited customization.