auth-setup logo

auth-setup

This covers OAuth setup for Google, GitHub, Apple, X, and custom providers, plus JWT lifetime tuning and post-login hooks.

SKILL.md

Full skill instructions

Butterbase Auth Setup

Two umbrella tools cover end-user authentication:

  • manage_oauth — provider configuration (Google, GitHub, Apple, X, custom)
  • manage_auth_config — auth hooks, JWT lifetimes, service key generation

For broad app build-out, see also butterbase-skills:build-app. This skill is the deep dive.


1. The role model

Every request runs under one of three database roles:

Auth headerRolecurrent_user_id()RLS
nonebutterbase_anonNULLenforced; default deny
End-user JWT (issued by manage_oauth or email login)butterbase_useruser UUIDenforced
Service key (bb_sk_*)butterbase_serviceNULLbypassed

Auth is what transforms a request into the right role. RLS is what filters the data. Both must be configured.


2. Configure an OAuth provider

manage_oauth({
  app_id: "app_abc123",
  action: "configure",
  provider: "google",
  client_id: "123456789.apps.googleusercontent.com",
  client_secret: "GOCSPX-...",
  redirect_uris: ["https://api.butterbase.ai/auth/app_abc123/oauth/google/callback"]
  // scopes / authorization_url / token_url / userinfo_url / provider_metadata are auto-filled for built-in providers
})

Built-in providers (URLs and scopes pre-filled): google, github, discord, facebook, linkedin, microsoft, apple, x.

Custom providers: pass authorization_url, token_url, userinfo_url, and scopes explicitly.

Redirect URI format

https://api.butterbase.ai/auth/{app_id}/oauth/{provider}/callback

Register this exact URI in the provider's developer console. Mismatch is the most common reason OAuth flows fail.

Provider quirks

ProviderQuirk
appleRequires provider_metadata: { teamId, keyId, privateKey }. Apple only returns the user's name on first auth and uses POST callback (handled automatically).
xDoes not return email. Butterbase synthesises {username}@users.noreply.x.local for the user record.
facebookDefault scopes email, public_profile.
googleStandard.
githubStandard.

List, update, delete

manage_oauth({ app_id, action: "get" })                          // list all providers (secrets redacted)
manage_oauth({ app_id, action: "get", provider: "google" })      // single provider
manage_oauth({ app_id, action: "update", provider: "google", client_secret: "new-secret" })
manage_oauth({ app_id, action: "delete", provider: "google" })   // disables future logins; existing sessions valid until expiry

Frontend flow

GET https://api.butterbase.ai/auth/{app_id}/oauth/{provider}?redirect_to=https://yourapp.com/auth/callback

User signs in at the provider, gets bounced back to redirect_to with access_token and refresh_token as query params. The Butterbase SDK wraps this:

await client.auth.signInWithOAuth({ provider: "google" });
const { user, accessToken } = await client.auth.getSession();

3. Tune JWT lifetimes

manage_auth_config({
  app_id: "app_abc123",
  action: "update_jwt",
  accessTokenTtl: "15m",       // formats: "15m", "1h", "2h", "1d"
  refreshTokenTtlDays: 30      // integer days
})

Defaults: 15-minute access tokens, 7-day refresh tokens.

Use caseaccessTokenTtlrefreshTokenTtlDays
High-security (banking, admin)5m15m17
Standard SaaS15m (default)30
Low-friction consumer apps1h90

Important: changes apply only to new tokens. Active tokens keep their original expiration — there is no global revoke. Treat TTL changes as forward-looking only.


4. Auth hooks (run code after every login)

A post-auth function is a deployed Butterbase function invoked fire-and-forget after every successful auth event (OAuth login, email login, email signup).

Wire it up

// 1. Deploy the function first (see butterbase-skills:function-dev)
deploy_function({
  app_id: "app_abc123",
  name: "after-auth",
  code: postAuthHandlerCode,
  trigger: { type: "http", config: { auth: "none" } }
})

// 2. Register it as the auth hook
manage_auth_config({
  app_id: "app_abc123",
  action: "configure_auth_hook",
  post_auth_function: "after-auth"
})

// To remove the hook later: pass post_auth_function: null

The function must already exist when you configure the hook.

Payload shape

The function receives a POST with this body:

{
  "event": "oauth_login | login | signup",
  "user": {
    "id": "uuid",
    "email": "...",
    "provider": "google | github | email | ...",
    "display_name": "...",
    "avatar_url": "..."
  },
  "isNewUser": true,
  "provider": "google"
}

The function runs as butterbase_service (RLS bypassed, ctx.user is null). Use body.user.id to know who just logged in.

Common uses

// after-auth/index.ts
export async function handler(req, ctx) {
  const { user, isNewUser, event } = await req.json();

  if (isNewUser) {
    // 1. Create profile row
    await ctx.db.query(
      "INSERT INTO profiles (user_id, display_name) VALUES ($1, $2) ON CONFLICT DO NOTHING",
      [user.id, user.display_name]
    );

    // 2. Send welcome email (via env-stored API key)
    ctx.waitUntil(sendWelcomeEmail(ctx.env.RESEND_API_KEY, user.email));
  }

  // 3. Audit log on every login
  await ctx.db.query(
    "INSERT INTO login_log (user_id, event, provider) VALUES ($1, $2, $3)",
    [user.id, event, ctx.user ?? null]
  );

  return new Response("ok", { status: 200 });
}

Auth hooks are fire-and-forget. Don't return data the user needs — they won't see it. Use them for side effects only.


5. Service keys (bb_sk_*)

Service keys grant full access to all your apps and bypass RLS. Treat them like passwords.

Generate

manage_auth_config({
  action: "generate_service_key",
  name: "CI/CD pipeline"
})
// → { key: "bb_sk_a1b2c3...", key_id, prefix, name, created_at }

The full key is returned once. Store it immediately in your secret manager — you cannot retrieve it again. If you lose it, generate a new one and revoke the old.

List & revoke

manage_api_keys({ action: "list" })
manage_api_keys({ action: "revoke", key_id: "uuid-..." })

list returns metadata only (prefix, name, last_used_at), never the secret. revoke is immediate and irreversible.

Rotation workflow

  1. manage_auth_config (generate_service_key) — create the new key.
  2. Update CI/CD, MCP config, scripts to use the new key.
  3. Verify with a smoke test (e.g. manage_app list).
  4. manage_api_keys (revoke) — kill the old key.

Do steps 1–3 before step 4 to avoid downtime.


6. Anti-patterns

Don'tDo
Hardcode bb_sk_* keys in client code or commit them to gitStore in env vars / secret manager
Reuse one OAuth app between dev, staging, prodSeparate OAuth apps per environment, with their own redirect URIs
Use a service key from frontend code "for convenience"Frontends use end-user JWTs; service keys are server-only
Increase accessTokenTtl to "fix" frequent re-authUse the refresh token; SDK handles this automatically
Forget that manage_oauth delete only stops new loginsExisting sessions remain valid until they expire — rotate JWT keys via support if you need a hard kill
Put critical logic in the auth hookHooks are fire-and-forget. Errors don't surface to the user. Keep them to side effects.
Log the full service key in audit / debug outputLog only the prefix (bb_sk_a1b2c3) — secrets must never appear in logs

7. Quick reference

TaskTool
Add Google OAuthmanage_oauth (configure)
List OAuth providersmanage_oauth (get)
Set post-login hookmanage_auth_config (configure_auth_hook)
Change JWT lifetimesmanage_auth_config (update_jwt)
Create service keymanage_auth_config (generate_service_key)
List service keysmanage_api_keys (list)
Revoke service keymanage_api_keys (revoke)

If a docs/butterbase/00-state.md exists in the working directory, prefer invoking via /butterbase-skills:journey-auth so the journey orchestrator stays in sync.