| name | heartwood-auth |
| description | Integrate Heartwood (GroveAuth) authentication into Grove applications. Use when adding sign-in, protecting routes, or validating sessions in any Grove property. |
Heartwood Auth Integration Skill
When to Activate
Activate this skill when:
- Adding authentication to a Grove application
- Protecting admin routes
- Validating user sessions
- Setting up OAuth sign-in
- Integrating with Heartwood (GroveAuth)
Overview
Heartwood is Grove's centralized authentication service powered by Better Auth.
| Domain | Purpose |
|---|
heartwood.grove.place | Frontend (login UI) |
auth-api.grove.place | Backend API |
Key Features
- OAuth Providers: Google
- Magic Links: Click-to-login emails via Resend
- Passkeys: WebAuthn passwordless authentication
- KV-Cached Sessions: Sub-100ms validation
- Cross-Subdomain SSO: Single session across all .grove.place
Integration Approaches
Option A: Better Auth Client (Recommended)
For new integrations, use Better Auth's client library:
import { createAuthClient } from "better-auth/client";
export const auth = createAuthClient({
baseURL: "https://auth-api.grove.place",
});
await auth.signIn.social({ provider: "google" });
const session = await auth.getSession();
await auth.signOut();
Option B: Cookie-Based SSO (*.grove.place apps)
For apps on .grove.place subdomains, sessions work automatically via cookies:
import type { Handle } from "@sveltejs/kit";
export const handle: Handle = async ({ event, resolve }) => {
const sessionCookie = event.cookies.get("better-auth.session_token");
if (sessionCookie) {
try {
const response = await fetch("https://auth-api.grove.place/api/auth/session", {
headers: {
Cookie: `better-auth.session_token=${sessionCookie}`,
},
});
if (response.ok) {
const data = await response.json();
event.locals.user = data.user;
event.locals.session = data.session;
}
} catch {
}
}
return resolve(event);
};
Option C: Legacy Token Flow (Backwards Compatible)
For existing integrations using the legacy OAuth flow:
const params = new URLSearchParams({
client_id: "your-client-id",
redirect_uri: "https://yourapp.grove.place/auth/callback",
state: crypto.randomUUID(),
code_challenge: await generateCodeChallenge(verifier),
code_challenge_method: "S256",
});
redirect(302, `https://auth-api.grove.place/login?${params}`);
const tokens = await fetch("https://auth-api.grove.place/token", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "authorization_code",
code: code,
redirect_uri: "https://yourapp.grove.place/auth/callback",
client_id: "your-client-id",
client_secret: env.HEARTWOOD_CLIENT_SECRET,
code_verifier: verifier,
}),
}).then((r) => r.json());
const user = await fetch("https://auth-api.grove.place/verify", {
headers: { Authorization: `Bearer ${tokens.access_token}` },
}).then((r) => r.json());
Protected Routes Pattern
SvelteKit Layout Protection
import { redirect } from "@sveltejs/kit";
import type { LayoutServerLoad } from "./$types";
export const load: LayoutServerLoad = async ({ locals }) => {
if (!locals.user) {
throw redirect(302, "/auth/login");
}
return {
user: locals.user,
};
};
API Route Protection
import { json, error } from "@sveltejs/kit";
import type { RequestHandler } from "./$types";
export const GET: RequestHandler = async ({ locals }) => {
if (!locals.user) {
throw error(401, "Unauthorized");
}
return json({ message: "Protected data", user: locals.user });
};
Session Validation
Via Better Auth Session Endpoint
async function validateSession(sessionToken: string) {
const response = await fetch("https://auth-api.grove.place/api/auth/session", {
headers: {
Cookie: `better-auth.session_token=${sessionToken}`,
},
});
if (!response.ok) return null;
const data = await response.json();
return data.session ? data : null;
}
Via Legacy Verify Endpoint
async function validateToken(accessToken: string) {
const response = await fetch("https://auth-api.grove.place/verify", {
headers: {
Authorization: `Bearer ${accessToken}`,
},
});
const data = await response.json();
return data.active ? data : null;
}
Client Registration
To integrate a new app with Heartwood, you need to register it as a client.
1. Generate Client Credentials
openssl rand -base64 32
echo -n "YOUR_SECRET" | openssl dgst -sha256 -binary | base64 | tr '+/' '-_' | tr -d '='
2. Register in Heartwood Database
INSERT INTO clients (id, name, client_id, client_secret_hash, redirect_uris, allowed_origins)
VALUES (
lower(hex(randomblob(16))),
'Your App Name',
'your-app-id',
'BASE64URL_HASHED_SECRET',
'["https://yourapp.grove.place/auth/callback"]',
'["https://yourapp.grove.place"]'
);
3. Set Secrets on Your App
wrangler secret put HEARTWOOD_CLIENT_SECRET
Environment Variables
| Variable | Description |
|---|
HEARTWOOD_CLIENT_ID | Your registered client ID |
HEARTWOOD_CLIENT_SECRET | Your client secret (never commit!) |
API Endpoints Reference
Better Auth Endpoints (Recommended)
| Method | Endpoint | Purpose |
|---|
| POST | /api/auth/sign-in/social | OAuth sign-in |
| GET | /api/auth/session | Get current session |
| POST | /api/auth/sign-out | Sign out |
Legacy Endpoints
| Method | Endpoint | Purpose |
|---|
| GET | /login | Login page |
| POST | /token | Exchange code for tokens |
| GET | /verify | Validate access token |
| GET | /userinfo | Get user info |
Best Practices
DO
- Use Better Auth client for new integrations
- Validate sessions on every protected request
- Use
httpOnly cookies for token storage
- Implement proper error handling for auth failures
- Log out users gracefully when sessions expire
DON'T
- Store tokens in localStorage (XSS vulnerable)
- Skip session validation on API routes
- Hardcode client secrets
- Ignore token expiration
Type-Safe Error Handling
Use Rootwork type guards in catch blocks instead of manual error type narrowing. Import from @autumnsgrove/lattice/server:
import { isRedirect, isHttpError } from "@autumnsgrove/lattice/server";
try {
} catch (err) {
if (isRedirect(err)) throw err;
if (isHttpError(err)) {
console.error(`Auth failed: ${err.status} ${err.body}`);
}
}
Reading session data from KV/cache: Use safeJsonParse() for type-safe deserialization:
import { safeJsonParse } from "@autumnsgrove/lattice/server";
import { z } from "zod";
const sessionSchema = z.object({
userId: z.string(),
email: z.string().email(),
});
const rawSession = await kv.get("session:123");
const session = safeJsonParse(rawSession, sessionSchema);
if (session) {
event.locals.user = { id: session.userId, email: session.email };
}
Cross-Subdomain SSO
All .grove.place apps share the same session cookie automatically:
better-auth.session_token (domain=.grove.place)
Once a user signs in on any Grove property, they're signed in everywhere.
Troubleshooting
"Session not found" errors
- Check cookie domain is
.grove.place
- Verify SESSION_KV namespace is accessible
- Check session hasn't expired
OAuth callback errors
- Verify redirect_uri matches registered client
- Check client_id is correct
- Ensure client_secret_hash uses base64url encoding
Slow authentication
- Ensure KV caching is enabled (SESSION_KV binding)
- Check for cold start issues (Workers may sleep)
Error Codes (HW-AUTH Catalog)
Heartwood has its own Signpost error catalog with 16 codes:
import {
AUTH_ERRORS,
getAuthError,
logAuthError,
buildErrorParams,
} from "@autumnsgrove/lattice/heartwood";
Key error codes:
| Code | Key | When |
|---|
HW-AUTH-001 | ACCESS_DENIED | User lacks permission |
HW-AUTH-002 | PROVIDER_ERROR | OAuth provider failed |
HW-AUTH-004 | REDIRECT_URI_MISMATCH | Callback URL doesn't match registered client |
HW-AUTH-020 | NO_SESSION | No session cookie found |
HW-AUTH-021 | SESSION_EXPIRED | Session timed out |
HW-AUTH-022 | INVALID_TOKEN | Token verification failed |
HW-AUTH-023 | TOKEN_EXCHANGE_FAILED | Code-for-token exchange failed |
Mapping OAuth errors to Signpost codes:
const authError = getAuthError(errorParam);
logAuthError(authError, { path: "/auth/callback", ip });
redirect(302, `/login?${buildErrorParams(authError)}`);
Number ranges: 001-019 infrastructure, 020-039 session/token, 040+ reserved.
See AgentUsage/error_handling.md for the full Signpost reference.
Related Resources
- Heartwood Spec:
/Users/autumn/Documents/Projects/GroveAuth/GROVEAUTH_SPEC.md
- Better Auth Docs: https://better-auth.com
- Client Setup Guide:
/Users/autumn/Documents/Projects/GroveAuth/docs/OAUTH_CLIENT_SETUP.md