Skip to main content
POST
/
admin
/
users
curl -X POST "https://auth-api.yourdomain.com/admin/users" \
  -H "Authorization: Bearer YOUR_SERVICE_ROLE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "newuser@example.com",
    "password": "SecurePassword123!",
    "email_confirm": true,
    "user_metadata": {
      "first_name": "John",
      "last_name": "Doe"
    },
    "app_metadata": {
      "role": "user",
      "department": "engineering"
    }
  }'
{
  "id": "123e4567-e89b-12d3-a456-426614174000",
  "aud": "authenticated",
  "role": "authenticated",
  "email": "newuser@example.com",
  "phone": null,
  "email_confirmed_at": "2023-12-01T10:30:00Z",
  "phone_confirmed_at": null,
  "last_sign_in_at": null,
  "app_metadata": {
    "role": "user",
    "department": "engineering"
  },
  "user_metadata": {
    "first_name": "John",
    "last_name": "Doe"
  },
  "created_at": "2023-12-01T10:30:00Z",
  "updated_at": "2023-12-01T10:30:00Z"
}

Overview

The /admin/users endpoint allows administrators to create new users programmatically. This endpoint requires admin privileges and can be used to create users with specific metadata, roles, and confirmation status.
This endpoint requires either a service role key or admin user JWT token for authentication.

Authentication

This endpoint supports two authentication methods:
  • Service Role Key: Authorization: Bearer <service-role-key>
  • Admin JWT: Authorization: Bearer <admin-jwt-token>

Request

curl -X POST "https://auth-api.yourdomain.com/admin/users" \
  -H "Authorization: Bearer YOUR_SERVICE_ROLE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "newuser@example.com",
    "password": "SecurePassword123!",
    "email_confirm": true,
    "user_metadata": {
      "first_name": "John",
      "last_name": "Doe"
    },
    "app_metadata": {
      "role": "user",
      "department": "engineering"
    }
  }'

Request Body

FieldTypeRequiredDescription
emailstringYesUser’s email address
passwordstringNoUser’s password (if not provided, user must set via recovery)
phonestringNoUser’s phone number in E.164 format
email_confirmbooleanNoWhether to mark email as confirmed (default: false)
phone_confirmbooleanNoWhether to mark phone as confirmed (default: false)
user_metadataobjectNoUser-editable metadata
app_metadataobjectNoApplication metadata (admin-only)

Response

{
  "id": "123e4567-e89b-12d3-a456-426614174000",
  "aud": "authenticated",
  "role": "authenticated",
  "email": "newuser@example.com",
  "phone": null,
  "email_confirmed_at": "2023-12-01T10:30:00Z",
  "phone_confirmed_at": null,
  "last_sign_in_at": null,
  "app_metadata": {
    "role": "user",
    "department": "engineering"
  },
  "user_metadata": {
    "first_name": "John",
    "last_name": "Doe"
  },
  "created_at": "2023-12-01T10:30:00Z",
  "updated_at": "2023-12-01T10:30:00Z"
}

Implementation Examples

Admin User Management Component

import { useState } from 'react';

function AdminUserCreation() {
  const [formData, setFormData] = useState({
    email: '',
    password: '',
    phone: '',
    email_confirm: false,
    phone_confirm: false,
    user_metadata: {
      first_name: '',
      last_name: ''
    },
    app_metadata: {
      role: 'user',
      department: ''
    }
  });
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  const [success, setSuccess] = useState(null);

  const handleSubmit = async (e) => {
    e.preventDefault();
    setLoading(true);
    setError(null);
    setSuccess(null);

    try {
      const response = await fetch('/api/admin/users', {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${serviceRoleKey}`,
          'Content-Type': 'application/json'
        },
        body: JSON.stringify(formData)
      });

      if (!response.ok) {
        const errorData = await response.json();
        throw new Error(errorData.msg || 'Failed to create user');
      }

      const user = await response.json();
      setSuccess(`User created successfully: ${user.email}`);
      
      // Reset form
      setFormData({
        email: '',
        password: '',
        phone: '',
        email_confirm: false,
        phone_confirm: false,
        user_metadata: { first_name: '', last_name: '' },
        app_metadata: { role: 'user', department: '' }
      });
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  };

  const handleInputChange = (e) => {
    const { name, value, type, checked } = e.target;
    
    if (name.includes('.')) {
      const [parent, child] = name.split('.');
      setFormData(prev => ({
        ...prev,
        [parent]: {
          ...prev[parent],
          [child]: type === 'checkbox' ? checked : value
        }
      }));
    } else {
      setFormData(prev => ({
        ...prev,
        [name]: type === 'checkbox' ? checked : value
      }));
    }
  };

  return (
    <div className="admin-user-creation">
      <h2>Create New User</h2>
      
      {error && (
        <div className="error-message">
          {error}
        </div>
      )}
      
      {success && (
        <div className="success-message">
          {success}
        </div>
      )}

      <form onSubmit={handleSubmit}>
        <div className="form-group">
          <label htmlFor="email">Email *</label>
          <input
            type="email"
            id="email"
            name="email"
            value={formData.email}
            onChange={handleInputChange}
            required
          />
        </div>

        <div className="form-group">
          <label htmlFor="password">Password</label>
          <input
            type="password"
            id="password"
            name="password"
            value={formData.password}
            onChange={handleInputChange}
            placeholder="Leave empty to require password reset"
          />
        </div>

        <div className="form-group">
          <label htmlFor="phone">Phone</label>
          <input
            type="tel"
            id="phone"
            name="phone"
            value={formData.phone}
            onChange={handleInputChange}
            placeholder="+1234567890"
          />
        </div>

        <div className="form-group">
          <label>
            <input
              type="checkbox"
              name="email_confirm"
              checked={formData.email_confirm}
              onChange={handleInputChange}
            />
            Mark email as confirmed
          </label>
        </div>

        <div className="form-group">
          <label>
            <input
              type="checkbox"
              name="phone_confirm"
              checked={formData.phone_confirm}
              onChange={handleInputChange}
            />
            Mark phone as confirmed
          </label>
        </div>

        <fieldset>
          <legend>User Metadata</legend>
          
          <div className="form-group">
            <label htmlFor="first_name">First Name</label>
            <input
              type="text"
              id="first_name"
              name="user_metadata.first_name"
              value={formData.user_metadata.first_name}
              onChange={handleInputChange}
            />
          </div>

          <div className="form-group">
            <label htmlFor="last_name">Last Name</label>
            <input
              type="text"
              id="last_name"
              name="user_metadata.last_name"
              value={formData.user_metadata.last_name}
              onChange={handleInputChange}
            />
          </div>
        </fieldset>

        <fieldset>
          <legend>App Metadata</legend>
          
          <div className="form-group">
            <label htmlFor="role">Role</label>
            <select
              id="role"
              name="app_metadata.role"
              value={formData.app_metadata.role}
              onChange={handleInputChange}
            >
              <option value="user">User</option>
              <option value="admin">Admin</option>
              <option value="moderator">Moderator</option>
            </select>
          </div>

          <div className="form-group">
            <label htmlFor="department">Department</label>
            <input
              type="text"
              id="department"
              name="app_metadata.department"
              value={formData.app_metadata.department}
              onChange={handleInputChange}
            />
          </div>
        </fieldset>

        <button type="submit" disabled={loading}>
          {loading ? 'Creating User...' : 'Create User'}
        </button>
      </form>
    </div>
  );
}

Bulk User Creation

import { useState } from 'react';

function BulkUserCreation() {
  const [csvData, setCsvData] = useState('');
  const [results, setResults] = useState([]);
  const [loading, setLoading] = useState(false);

  const handleBulkCreate = async () => {
    setLoading(true);
    setResults([]);

    const lines = csvData.trim().split('\n');
    const headers = lines[0].split(',').map(h => h.trim());
    const users = lines.slice(1).map(line => {
      const values = line.split(',').map(v => v.trim());
      const user = {};
      headers.forEach((header, index) => {
        if (values[index]) {
          user[header] = values[index];
        }
      });
      return user;
    });

    const creationResults = [];

    for (const user of users) {
      try {
        const response = await fetch('/api/admin/users', {
          method: 'POST',
          headers: {
            'Authorization': `Bearer ${serviceRoleKey}`,
            'Content-Type': 'application/json'
          },
          body: JSON.stringify({
            ...user,
            email_confirm: user.email_confirm === 'true',
            user_metadata: {
              first_name: user.first_name,
              last_name: user.last_name
            },
            app_metadata: {
              role: user.role || 'user',
              department: user.department
            }
          })
        });

        if (response.ok) {
          const createdUser = await response.json();
          creationResults.push({
            email: user.email,
            status: 'success',
            user: createdUser
          });
        } else {
          const error = await response.json();
          creationResults.push({
            email: user.email,
            status: 'error',
            error: error.msg
          });
        }
      } catch (error) {
        creationResults.push({
          email: user.email,
          status: 'error',
          error: error.message
        });
      }
    }

    setResults(creationResults);
    setLoading(false);
  };

  return (
    <div className="bulk-user-creation">
      <h2>Bulk User Creation</h2>
      
      <div className="csv-input">
        <label htmlFor="csv-data">CSV Data</label>
        <textarea
          id="csv-data"
          value={csvData}
          onChange={(e) => setCsvData(e.target.value)}
          placeholder="email,password,first_name,last_name,role,department,email_confirm
user1@example.com,password123,John,Doe,user,engineering,true
user2@example.com,password456,Jane,Smith,admin,marketing,true"
          rows={10}
          cols={80}
        />
      </div>

      <button onClick={handleBulkCreate} disabled={loading || !csvData.trim()}>
        {loading ? 'Creating Users...' : 'Create Users'}
      </button>

      {results.length > 0 && (
        <div className="results">
          <h3>Results</h3>
          <table>
            <thead>
              <tr>
                <th>Email</th>
                <th>Status</th>
                <th>Details</th>
              </tr>
            </thead>
            <tbody>
              {results.map((result, index) => (
                <tr key={index} className={result.status}>
                  <td>{result.email}</td>
                  <td>{result.status}</td>
                  <td>
                    {result.status === 'success' 
                      ? `User ID: ${result.user.id}`
                      : result.error
                    }
                  </td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      )}
    </div>
  );
}

Node.js Admin Service

const express = require('express');
const { body, validationResult } = require('express-validator');

// Middleware to check admin privileges
function requireAdmin(req, res, next) {
  const user = req.user;
  
  // Check if using service role key
  if (req.headers.authorization?.includes('service-role-key')) {
    return next();
  }
  
  // Check if user has admin role
  if (!user || user.app_metadata?.role !== 'admin') {
    return res.status(403).json({
      code: 403,
      msg: 'Insufficient privileges',
      details: 'Admin privileges required'
    });
  }
  
  next();
}

// Validation rules for user creation
const createUserValidation = [
  body('email').isEmail().normalizeEmail(),
  body('password').optional().isLength({ min: 8 }),
  body('phone').optional().isMobilePhone(),
  body('email_confirm').optional().isBoolean(),
  body('phone_confirm').optional().isBoolean(),
  body('user_metadata').optional().isObject(),
  body('app_metadata').optional().isObject()
];

// Create user endpoint
app.post('/admin/users', 
  authenticateToken,
  requireAdmin,
  createUserValidation,
  async (req, res) => {
    try {
      const errors = validationResult(req);
      if (!errors.isEmpty()) {
        return res.status(400).json({
          code: 400,
          msg: 'Invalid request data',
          details: errors.array()[0].msg
        });
      }

      const {
        email,
        password,
        phone,
        email_confirm = false,
        phone_confirm = false,
        user_metadata = {},
        app_metadata = {}
      } = req.body;

      // Check if user already exists
      const existingUser = await supabase.auth.admin.getUserByEmail(email);
      if (existingUser.data.user) {
        return res.status(409).json({
          code: 409,
          msg: 'User already exists',
          details: 'A user with this email already exists'
        });
      }

      // Create user
      const { data, error } = await supabase.auth.admin.createUser({
        email,
        password,
        phone,
        email_confirm,
        phone_confirm,
        user_metadata,
        app_metadata
      });

      if (error) {
        return res.status(400).json({
          code: 400,
          msg: 'Failed to create user',
          details: error.message
        });
      }

      // Log admin action
      console.log(`Admin ${req.user.sub} created user ${data.user.id}`);

      res.json(data.user);
    } catch (error) {
      console.error('Create user error:', error);
      res.status(500).json({
        code: 500,
        msg: 'Internal server error'
      });
    }
  }
);

// Bulk user creation endpoint
app.post('/admin/users/bulk',
  authenticateToken,
  requireAdmin,
  async (req, res) => {
    try {
      const { users } = req.body;
      
      if (!Array.isArray(users) || users.length === 0) {
        return res.status(400).json({
          code: 400,
          msg: 'Invalid request data',
          details: 'Users array is required'
        });
      }

      const results = [];
      
      for (const userData of users) {
        try {
          const { data, error } = await supabase.auth.admin.createUser(userData);
          
          if (error) {
            results.push({
              email: userData.email,
              status: 'error',
              error: error.message
            });
          } else {
            results.push({
              email: userData.email,
              status: 'success',
              user: data.user
            });
          }
        } catch (error) {
          results.push({
            email: userData.email,
            status: 'error',
            error: error.message
          });
        }
      }

      res.json({ results });
    } catch (error) {
      res.status(500).json({
        code: 500,
        msg: 'Internal server error'
      });
    }
  }
);

Use Cases

User Onboarding

  • Create users for new employees
  • Set up accounts with specific roles and departments
  • Pre-confirm email addresses for trusted domains

Bulk Operations

  • Import users from existing systems
  • Create test accounts for development
  • Set up demo accounts with sample data

Administrative Tasks

  • Create admin accounts with elevated privileges
  • Set up service accounts for integrations
  • Create users with specific metadata requirements

Security Considerations

  • Authentication: Requires service role key or admin JWT
  • Authorization: Verify admin privileges before allowing user creation
  • Input Validation: Validate all input data thoroughly
  • Audit Logging: Log all admin user creation activities
  • Rate Limiting: Implement rate limiting to prevent abuse

Best Practices

  • Password Policy: Enforce strong password requirements
  • Metadata Structure: Use consistent metadata schemas
  • Error Handling: Provide clear error messages
  • Bulk Operations: Implement proper error handling for bulk creation
  • Monitoring: Monitor admin activities for security

Rate Limiting

  • Endpoint: 100 requests per hour per admin
  • Bulk Endpoint: 10 requests per hour per admin
  • Purpose: Prevent abuse while allowing legitimate admin operations

Testing

// Jest test example
describe('Admin User Creation', () => {
  test('should create user with service role key', async () => {
    const userData = {
      email: 'test@example.com',
      password: 'SecurePassword123!',
      email_confirm: true,
      user_metadata: { first_name: 'Test' },
      app_metadata: { role: 'user' }
    };

    const response = await request(app)
      .post('/admin/users')
      .set('Authorization', `Bearer ${serviceRoleKey}`)
      .send(userData);

    expect(response.status).toBe(200);
    expect(response.body).toHaveProperty('id');
    expect(response.body.email).toBe(userData.email);
  });

  test('should fail without admin privileges', async () => {
    const response = await request(app)
      .post('/admin/users')
      .set('Authorization', `Bearer ${userToken}`)
      .send({ email: 'test@example.com' });

    expect(response.status).toBe(403);
  });

  test('should handle duplicate email', async () => {
    const userData = { email: 'existing@example.com' };

    // Create user first
    await request(app)
      .post('/admin/users')
      .set('Authorization', `Bearer ${serviceRoleKey}`)
      .send(userData);

    // Try to create again
    const response = await request(app)
      .post('/admin/users')
      .set('Authorization', `Bearer ${serviceRoleKey}`)
      .send(userData);

    expect(response.status).toBe(409);
    expect(response.body.msg).toContain('already exists');
  });
});
I