Authentication
Miranda uses Supabase Auth for identity and JWT-based authentication. All authenticated API requests require a valid Supabase JWT in the Authorization header.
Authentication flow
Section titled “Authentication flow”- Sign in via Supabase — Use the Supabase client library to authenticate (email/password, magic link, or OAuth)
- Obtain JWT — Supabase returns an
access_token(JWT) - Send requests — Include the JWT as a Bearer token in API requests
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"Request headers
Section titled “Request headers”| Header | Required | Description |
|---|---|---|
Authorization | Yes | Bearer {supabase_jwt} |
X-Foundation-Id | Conditional | Required for multi-foundation users to select active foundation context |
Content-Type | For POST/PUT | application/json |
Token structure
Section titled “Token structure”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
Claims used
Section titled “Claims used”| Claim | Purpose |
|---|---|
sub | Supabase user UUID — maps to User.auth_user_id |
email | User’s email address |
app_metadata.roles | Application roles (e.g., ["donor"]) |
user_metadata.full_name | Display name (used during registration) |
Auth middleware modes
Section titled “Auth middleware modes”Strict mode (default)
Section titled “Strict mode (default)”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.
Relaxed mode
Section titled “Relaxed mode”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.
Multi-foundation context
Section titled “Multi-foundation context”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:
# List all foundations the user belongs toGET /api/my-foundationsAuthorization: Bearer {jwt}
# Switch to a specific foundationGET /api/meAuthorization: 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.
Registration
Section titled “Registration”New users register by authenticating with Supabase first, then calling the registration endpoint:
POST /api/registerAuthorization: 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.
Invite-based authentication
Section titled “Invite-based authentication”Team members can join a foundation through invitations:
| Endpoint | Auth | Description |
|---|---|---|
GET /api/team/invites/{token}/details | None | Get invite details (foundation name, role, email) |
POST /api/team/invites/{token}/accept | Optional JWT | Accept invite, create User record |
POST /api/team/invites/{token}/accept-and-login | None | Accept 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.
Donor authentication
Section titled “Donor authentication”Donors authenticate via Supabase with the donor role in their app_metadata.roles:
GET /api/donor/profileAuthorization: Bearer {jwt_with_donor_role}The EnsureRole middleware validates that the token includes the required role.
Error responses
Section titled “Error responses”| Status | Meaning |
|---|---|
| 401 | Missing or invalid token, or user not found (strict mode) |
| 403 | Token valid but insufficient role, or no foundation context |
| 409 | User 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."}