heartwood-auth
Integrate Heartwood (GroveAuth) authentication into Grove applications. Use when adding sign-in, protecting routes, or validating sessions in any Grove property.
Installation
Follow the repository instructions for this skill and your agent. Review the files and dependencies before adding it to your workflow.
View source ↗GuidesSkill instructions
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:
// src/lib/auth/client.ts
import { createAuthClient } from 'better-auth/client';
export const auth = createAuthClient({
baseURL: 'https://auth-api.grove.place'
});
// Sign in with Google
await auth.signIn.social({ provider: 'google' });
// Get current session
const session = await auth.getSession();
// Sign out
await auth.signOut();
Option B: Cookie-Based SSO (*.grove.place apps)
For apps on .grove.place subdomains, sessions work automatically via cookies:
// src/hooks.server.ts
import type { Handle } from '@sveltejs/kit';
export const handle: Handle = async ({ event, resolve }) => {
// Check session via Heartwood API
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 {
// Session invalid or expired
}
}
return resolve(event);
};
Option C: Legacy Token Flow (Backwards Compatible)
For existing integrations using the legacy OAuth flow:
// 1. Redirect to Heartwood login
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}`);
// 2. Exchange code for tokens (in callback route)
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());
// 3. Verify token on protected routes
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
// src/routes/admin/+layout.server.ts
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
// src/routes/api/protected/+server.ts
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
# Generate a secure client secret
openssl rand -base64 32
# Example: YKzJChC3RPjZvd1f/OD5zUGAvcouOTXG7maQP1ernCg=
# Hash it for storage (base64url encoding)
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
# Set the client secret on your app
wrangler secret put HEARTWOOD_CLIENT_SECRET
# Paste: YKzJChC3RPjZvd1f/OD5zUGAvcouOTXG7maQP1ernCg=
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 |
| POST | /api/auth/sign-in/magic-link |
Magic link sign-in |
| POST | /api/auth/sign-in/passkey |
Passkey 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
httpOnlycookies 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
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)
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
Examples
Read the usage examples in the source repository.
View source ↗Source history
- Indexed on
- 2026-01-20
- Index updated
- 2026-01-26
- Branch
- main
Index and sync dates describe this directory. View the source repository for code history.
GitHub ↗chat Comments (0)
Sign in to join the discussion and leave a comment.
No comments yet. Be the first to share your thoughts!