home / skills / autumnsgrove / groveengine / heartwood-auth

heartwood-auth skill

/.claude/skills/heartwood-auth

This skill integrates Heartwood authentication into Grove apps, enabling secure sign-in, protected routes, and seamless session validation for users.

npx playbooks add skill autumnsgrove/groveengine --skill heartwood-auth

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
7.9 KB
---
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:

```typescript
// 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:

```typescript
// 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:

```typescript
// 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

```typescript
// 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

```typescript
// 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

```typescript
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

```typescript
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

```bash
# 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

```sql
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

```bash
# 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 `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

## 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`

Overview

This skill integrates Heartwood (GroveAuth) authentication into Grove applications, providing sign-in, session validation, and route protection for multi-tenant Grove properties. It supports modern flows (Better Auth client, OAuth, magic links, and passkeys) and enables cross-subdomain SSO across .grove.place. Use it to add secure, fast auth to SvelteKit or other TypeScript Grove apps.

How this skill works

The skill wires your app to Heartwood’s frontend and auth-api endpoints, using either the Better Auth client library or direct API calls. It validates sessions via a KV-cached session endpoint or legacy token verify endpoint, populates locals (user/session) on server requests, and enforces protection on layouts and API routes. Client registration and secrets are handled via environment variables and registered client records.

When to use it

  • Adding authentication or single sign-on to a Grove application
  • Protecting admin dashboards, API endpoints, or server-rendered routes
  • Validating user sessions on every protected request
  • Implementing OAuth sign-in, magic links, or passkeys
  • Migrating from a legacy OAuth token flow to the Better Auth client

Best practices

  • Prefer the Better Auth client for new integrations to simplify sign-in and session handling
  • Validate sessions on every protected API or server route and populate locals with user/session data
  • Store secrets in environment variables and never commit client secrets to source control
  • Use httpOnly cookies for session tokens and avoid localStorage to reduce XSS risk
  • Implement graceful logout and error handling for expired or invalid sessions

Example use cases

  • Protect a SvelteKit admin layout by redirecting unauthenticated users to /auth/login
  • Auto-validate session cookies on every request by calling /api/auth/session and attaching user to locals
  • Add Google OAuth sign-in using auth.signIn.social with the Better Auth client
  • Support passwordless login via magic links and passkeys for faster user onboarding
  • Register a new client in Heartwood, set HEARTWOOD_CLIENT_SECRET, and exchange codes in an OAuth callback

FAQ

Which integration approach should I pick?

Use the Better Auth client for new apps. Use cookie-based SSO for apps on .grove.place. Keep legacy token flow only for backwards compatibility.

How do I validate sessions quickly?

Call the /api/auth/session endpoint with the better-auth.session_token cookie or use the legacy /verify endpoint for access tokens. Enable KV caching (SESSION_KV) for sub-100ms validation.