Skip to content

Authentication

Miranda uses Supabase Auth for identity and JWT-based authentication. All authenticated API requests require a valid Supabase JWT in the Authorization header.

  1. Sign in via Supabase — Use the Supabase client library to authenticate (email/password, magic link, or OAuth)
  2. Obtain JWT — Supabase returns an access_token (JWT)
  3. Send requests — Include the JWT as a Bearer token in API requests
Terminal window
curl -X GET https://app.miranda.foundation/api/me \
-H "Authorization: Bearer {supabase_jwt}" \
-H "X-Foundation-Id: {foundation_uuid}" \
-H "Content-Type: application/json"
HeaderRequiredDescription
AuthorizationYesBearer {supabase_jwt}
X-Foundation-IdConditionalRequired for multi-foundation users to select active foundation context
Content-TypeFor POST/PUTapplication/json

Miranda validates Supabase JWTs using the SupabaseAuthService:

  • Production: JWKS-based verification (RS256/ES256) via {SUPABASE_URL}/auth/v1/.well-known/jwks.json
  • Testing: HMAC verification (HS256) via SUPABASE_JWT_SECRET
ClaimPurpose
subSupabase user UUID — maps to User.auth_user_id
emailUser’s email address
app_metadata.rolesApplication roles (e.g., ["donor"])
user_metadata.full_nameDisplay name (used during registration)
Route::middleware('auth.supabase')->group(...)

Requires a valid JWT and an existing User record matching the token’s sub claim. Returns 401 if the user is not found.

Route::middleware('auth.supabase:relaxed')->group(...)

Requires a valid JWT but does not require an existing User record. Used for registration and donor endpoints where the user may not exist in Miranda yet. The decoded token is stored on the request for downstream use.

A single Supabase auth user can belong to multiple foundations (one User record per foundation). The X-Foundation-Id header selects which foundation context to use:

Terminal window
# List all foundations the user belongs to
GET /api/my-foundations
Authorization: Bearer {jwt}
# Switch to a specific foundation
GET /api/me
Authorization: Bearer {jwt}
X-Foundation-Id: {foundation_uuid}

If X-Foundation-Id is not provided, the middleware returns the first matching User record for the auth user.

New users register by authenticating with Supabase first, then calling the registration endpoint:

Terminal window
POST /api/register
Authorization: Bearer {supabase_jwt}
Content-Type: application/json
{
"foundation_name": "My Foundation",
"org_type": "foundation"
}

This creates both a Foundation and a User record (with role: owner). The foundation gets a 14-day trial.

Supported org_type values are defined in config/org-types.php.

Team members can join a foundation through invitations:

EndpointAuthDescription
GET /api/team/invites/{token}/detailsNoneGet invite details (foundation name, role, email)
POST /api/team/invites/{token}/acceptOptional JWTAccept invite, create User record
POST /api/team/invites/{token}/accept-and-loginNoneAccept invite and receive session tokens

The accept-and-login endpoint creates a Supabase account (if needed), signs in, and returns access_token and refresh_token for immediate use.

Donors authenticate via Supabase with the donor role in their app_metadata.roles:

Terminal window
GET /api/donor/profile
Authorization: Bearer {jwt_with_donor_role}

The EnsureRole middleware validates that the token includes the required role.

StatusMeaning
401Missing or invalid token, or user not found (strict mode)
403Token valid but insufficient role, or no foundation context
409User already registered (duplicate registration attempt)
// 401 - Missing token
{"message": "Unauthenticated."}
// 401 - Invalid token
{"message": "Invalid token."}
// 401 - User not found (strict mode)
{"message": "User not found."}
// 403 - No foundation context
{"message": "No foundation context. User must belong to a foundation."}
// 403 - Insufficient role
{"message": "Insufficient role."}