adminUsers Edge Function
REST API for admin user management operations. Provides endpoints for managing the waiting list, users, and authentication links.
Last Updated: January 15, 2026
Base URL
POST/GET https://<supabase-project>.supabase.co/functions/v1/adminUsers/<endpoint>
Authentication
All endpoints require a valid JWT token with admin access level (9+):
Authorization: Bearer <jwt_token>
The JWT must contain app_claims.access_level >= 9.
Endpoints
Statistics
GET /stats
Get waiting list and user statistics.
Response:
{
"pending": 5,
"approved": 100,
"rejected": 10,
"expired": 2,
"totalUsers": 95
}
Waiting List
GET /waiting-list
Get waiting list entries with optional status filter.
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
status |
string | Filter by: pending, approved, rejected, expired |
Response:
{
"entries": [
{
"id": "uuid",
"email": "user@example.com",
"full_name": "John Doe",
"status": "pending",
"signup_source": "web",
"created_at": "2026-01-15T00:00:00Z"
}
]
}
User Management
GET /users
Get all users with their email addresses.
Response:
{
"users": [
{
"id": "uuid",
"auth_id": "auth-uuid",
"email": "user@example.com",
"full_name": "John Doe",
"access_level": 5,
"org_id": "fpp",
"active": true,
"created_at": "2026-01-15T00:00:00Z"
}
]
}
POST /lookup-user
Look up a user by email address.
Body:
{
"email": "user@example.com"
}
Response:
{
"authUser": {
"id": "auth-uuid",
"email": "user@example.com"
},
"user": {
"id": "public-user-uuid",
"full_name": "John Doe",
"access_level": 5,
"org_id": "fpp"
}
}
POST /delete-user
Delete a user from both public.users and auth.users.
Body:
{
"userId": "public-user-uuid"
}
Response:
{
"message": "User deleted successfully",
"deletedPublicUser": true,
"deletedAuthUser": true
}
Approval Workflow
POST /approve
Approve a waiting list entry and create a user account.
Body:
{
"entryId": "waiting-list-uuid",
"accessLevel": 5,
"orgId": "fpp",
"transferDocs": true,
"redirectTo": "https://your-app.com/welcome"
}
| Field | Type | Default | Description |
|---|---|---|---|
entryId |
string | required | Waiting list entry ID |
accessLevel |
number | 5 | User's access level (1-9) |
orgId |
string | "fpp" | Organization ID |
transferDocs |
boolean | false | Transfer documents created on behalf of this user |
redirectTo |
string | - | Redirect URL after user sets password |
Response:
{
"message": "User approved successfully",
"user": {
"id": "public-user-uuid",
"auth_id": "auth-uuid",
"access_level": 5,
"org_id": "fpp"
},
"documentsTransferred": 0,
"inviteLink": "https://xxx.supabase.co/auth/v1/verify?token=..."
}
Important: The inviteLink is a one-time use link. Share it with the user so they can set their password. If you miss it, use /generate-link to create a new one.
POST /reject
Reject a waiting list entry.
Body:
{
"entryId": "waiting-list-uuid",
"reason": "Optional rejection reason"
}
POST /delete-waiting-list-entry
Delete a waiting list entry (rejected/expired only).
Body:
{
"entryId": "waiting-list-uuid"
}
Note: Cannot delete pending or approved entries.
Authentication Links
POST /generate-link
Generate an invite or recovery link for a user. Use this if you missed the inviteLink from approval or need to send a new password reset link.
Body:
{
"email": "user@example.com",
"type": "invite",
"redirectTo": "https://your-app.com/welcome"
}
| Field | Type | Default | Description |
|---|---|---|---|
email |
string | required | User's email address |
type |
string | "invite" | Link type: invite or recovery |
redirectTo |
string | - | Redirect URL after authentication |
Link Types:
| Type | Use Case | Creates User? |
|---|---|---|
invite |
New users who need to set their password | Yes (if not exists) |
recovery |
Existing users who forgot their password | No |
Response:
{
"message": "Generated invite link for user@example.com",
"link": "https://xxx.supabase.co/auth/v1/verify?token=...",
"type": "invite",
"email": "user@example.com"
}
Example Usage:
# Generate invite link for newly approved user
curl -X POST https://xxx.supabase.co/functions/v1/adminUsers/generate-link \
-H "Authorization: Bearer YOUR_ADMIN_JWT" \
-H "Content-Type: application/json" \
-d '{"email": "user@example.com", "type": "invite"}'
# Generate password reset link for existing user
curl -X POST https://xxx.supabase.co/functions/v1/adminUsers/generate-link \
-H "Authorization: Bearer YOUR_ADMIN_JWT" \
-H "Content-Type: application/json" \
-d '{"email": "user@example.com", "type": "recovery"}'
Document Transfer
Note: Document transfer endpoints use the admin user's org_id from their JWT claims to determine which document table to search/update. The table name is derived using the pattern: ${SUPABASE_VECTOR_TABLENAME}_${org_id} (e.g., documents_ctrl-shift for org_id: "ctrl-shift").
GET /pending-docs
Preview documents pending transfer for an email.
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
email |
string | Email to search in document metadata (onBehalfOf) |
Example:
GET /pending-docs?email=user@example.com
Response:
{
"documents": [
{
"id": "doc-uuid",
"metadata": { "onBehalfOf": "user@example.com" },
"user_id": "original-owner-uuid"
}
],
"total": 5,
"tableName": "documents_ctrl-shift"
}
POST /transfer-docs
Transfer document ownership to a user.
Body:
{
"email": "user@example.com",
"newOwnerId": "public-user-uuid"
}
| Field | Type | Description |
|---|---|---|
email |
string | Email to search in document metadata |
newOwnerId |
string | Target user's public.users.id |
Response:
{
"message": "Transferred 5 documents",
"transferred": 5,
"tableName": "documents_ctrl-shift"
}
Performance:
- Uses
admin_transfer_documentsRPC for bulk transfers (single DB call) - 5500+ documents transfer in ~8 seconds (vs ~90 seconds with individual updates)
- The transfer is idempotent - re-running updates remaining rows to same
user_id
Supabase Auth Password Requirements
When users set their password via invite/recovery links, Supabase enforces these requirements:
| Requirement | Value | Notes |
|---|---|---|
| Minimum length | 6 characters (default) | Configurable in Supabase Dashboard |
| Maximum length | 72 characters | Hard limit |
| Recommended minimum | 8+ characters | Security best practice |
Configurable Options (Supabase Dashboard → Auth → Providers → Email)
- Minimum password length
- Required character types (digits, lowercase, uppercase, symbols)
- Leaked password protection (Pro plan+)
UI Hint Text
For frontend password fields, use:
Password must be at least 6 characters (8+ recommended)
Sources:
Error Responses
All errors return:
{
"error": "Error message description"
}
| Status | Description |
|---|---|
| 400 | Bad request (missing required fields) |
| 401 | Unauthorized (missing/invalid JWT) |
| 403 | Forbidden (access level < 9) |
| 404 | Not found (user/entry not found) |
| 500 | Server error |
Typical Workflow
Approving a New User
- User signs up via waiting list form → stored in
waiting_listtable - Admin reviews pending entries via
GET /waiting-list?status=pending - Admin approves via
POST /approve→ receivesinviteLink - Admin shares link with user via email/chat
- User clicks link → sets their password → can now sign in
If Invite Link is Lost
- Call
POST /generate-linkwithtype: "invite"ortype: "recovery" - Share the new link with the user
- Links are single-use; generate a new one if needed
Resetting an Existing User's Password
- Call
POST /generate-linkwithtype: "recovery" - Share the recovery link with the user
- User clicks link → sets new password
Related Functions
registerUser- Public endpoint for waiting list signuprag- RAG endpoint that users access after approval
Related UIs
ui/auth/admin.html- Admin dashboard for user managementsupabase/functions/_supabase-auth/test-user-management.html- Developer testing UI
Change Log
| Date | Description |
|---|---|
| 2026-01-15 | Created users_with_email view and updated /users, /lookup-user, /approve, /generate-link to use it instead of slow listUsers() |
| 2026-01-15 | Optimized /transfer-docs to use admin_transfer_documents RPC for ~37x performance improvement (5500 docs in 8s vs 1500 in 90s) |
| 2026-01-15 | Added pagination to /pending-docs to handle >1000 documents (Supabase default limit) |
| 2026-01-15 | Refactored /pending-docs and /transfer-docs to use org_id from JWT claims (via getTableName()) instead of explicit tableName parameter |
| 2026-01-15 | Added POST /generate-link endpoint for invite/recovery link generation |
| 2026-01-15 | Updated POST /approve to use invite links instead of random passwords |
| 2026-01-15 | Added inviteLink to approval response |
| 2026-01-15 | Initial README documentation |