BIO.RE
Authentication

Register New Account

Create a new BIO.RE account — email + password + terms acceptance, with optional username, OAuth attribution, and locale.

POST /api/v1/auth/register — 🌐 Public · Rate limit: 10 req / hour · Captcha: required

Creates a new user account. Sends a verification email. Returns the new userId so the client can navigate to the email verification flow. Username is optional at registration; users can claim a username later.

This endpoint enforces the platform-wide kill switch platform.registration_enabled (admin-managed). When the switch is off, registration returns 403 Forbidden with i18nKey auth.register.closed.

Request

Headers

HeaderValueNotes
Content-Typeapplication/jsonRequired
User-Agent(auto)Captured into User.registrationDevice
Accept-Language(auto)Used as fallback when body.locale is empty
cf-connecting-ip(auto, Cloudflare)Source for GeoIP User.registrationCountry

Body — RegisterDto

FieldTypeRequiredValidationNotes
emailstring (RFC 5322)Trimmed + lowercased; disposable domain blocked; MX record checkedPersisted to User.email (unique)
passwordstring8–128 chars; must include upper + lower + digitbcrypt hashed (salt rounds from config auth.salt_rounds, min 10)
acceptedTermsbooleanMust equal trueGDPR consent record written
acceptedPrivacybooleanMust equal trueGDPR consent record written
usernamestring1–100 chars, ^[a-z0-9._-]+$Unique across User + ReservedUsername
displayNamestringmax 100 charsUI display (separate from username)
intent'creator' | 'fan'max 20 charsSets User.intent; null = undecided
captchaTokenstringValidated by CaptchaGuard against the active captcha provider (admin-managed via external.captcha.active_provider)Required when captcha is enabled
referralCodestringResolves against User.referralCode first, then ReferralLink.codeSets User.referredBy
localestringMust be in platform.supported_localesFalls back to Accept-Language then platform default
utmSource utmMedium utmCampaign utmTerm utmContentstring × 5max 100 chars eachPermanent attribution
firstReferrerUrlstringmax 2048 charsPermanent attribution
firstLandingPagestringmax 2048 charsPermanent attribution

Field turnstileToken is deprecated — accepted as a backwards-compat alias for captchaToken for one sprint. Do not use in new code; the active provider is admin-selected via external.captcha.active_provider.

Response

201 Created — success

{
  "success": true,
  "data": {
    "userId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "message": "Registration successful. Please check your email to verify your account."
  }
}

Errors

HTTPcode / i18nKeyReason
400auth.register.invalid_emailDisposable domain, invalid MX, or otherwise rejected by EmailValidatorService
400(DTO validation array)class-validator failure (password policy, terms not accepted, etc.)
403auth.register.closedPlatform kill switch platform.registration_enabled = false
409auth.register.email_existsEmail already registered
409auth.register.account_previously_deletedEmail matches a GDPR-deleted tombstone (SHA-256 hash)
409auth.register.username_unavailableUsername taken in User or ReservedUsername
409auth.register.referral_code_collisionGenerated referral code collided 3 times against ReferralLink.code (rare; client can retry)
429(throttle)Rate limit exceeded (10 req/hour)

All non-validation errors follow the platform error envelope:

{ "success": false, "error": { "code": "auth.register.email_exists", "message": "Email already registered", "i18nKey": "auth.register.email_exists", "correlationId": "..." } }

Side effects

  1. Kill-switch checkplatform.registration_enabled (ConfigService).
  2. Email validation — disposable domain + MX record check (EmailValidatorService).
  3. Email uniqueness — Prisma User.email unique.
  4. GDPR tombstone check — SHA-256 of email matched against email = 'deleted_<hash>@deleted.bio.re' and status = DELETED.
  5. Username uniqueness — both User.username and ReservedUsername.username.
  6. Referral resolutionUser.referralCode first, fallback to ReferralLink.code.
  7. Password hashing — bcrypt, configurable salt rounds (min 10).
  8. Atomic transactionUser insert + EmailVerification insert + ConsentRecord (terms, privacy) insert.
  9. Cross-table referral code uniqueness — generated code retried up to 3 times against ReferralLink.code.
  10. GeoIP lookup — non-blocking; populates User.registrationCountry.
  11. Welcome / verification email — queued to worker-service, dispatched via the active email provider (admin-managed via external.email.active_provider; failover handled server-side).

Code samples

curl -X POST https://api.bio.re/api/v1/auth/register \
  -H 'Content-Type: application/json' \
  -H 'Accept-Language: en' \
  -d '{
    "email": "[email protected]",
    "username": "creator",
    "password": "SecureP@ss123",
    "acceptedTerms": true,
    "acceptedPrivacy": true,
    "displayName": "Awesome Creator",
    "intent": "creator",
    "captchaToken": "<turnstile_token>",
    "locale": "en"
  }'
type RegisterRequest = {
  email: string;
  password: string;
  acceptedTerms: true;
  acceptedPrivacy: true;
  username?: string;
  displayName?: string;
  intent?: 'creator' | 'fan';
  captchaToken?: string;
  referralCode?: string;
  locale?: string;
};

type RegisterResponse = { userId: string; message: string };

async function register(input: RegisterRequest): Promise<RegisterResponse> {
  const res = await fetch('https://api.bio.re/api/v1/auth/register', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(input),
  });
  const json = await res.json();
  if (!res.ok || !json.success) {
    const code = json?.error?.code ?? `http_${res.status}`;
    throw Object.assign(new Error(json?.error?.message ?? 'Register failed'), { code });
  }
  return json.data as RegisterResponse;
}
import { useMutation } from '@tanstack/react-query';

export function useRegister() {
  return useMutation({
    mutationFn: async (input: RegisterRequest) => {
      const res = await fetch('/api/v1/auth/register', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(input),
      });
      const json = await res.json();
      if (!res.ok || !json.success) {
        throw Object.assign(new Error(json?.error?.message ?? 'Register failed'), {
          code: json?.error?.code ?? `http_${res.status}`,
          i18nKey: json?.error?.i18nKey,
        });
      }
      return json.data as RegisterResponse;
    },
    onSuccess: ({ userId }) => {
      // Navigate to email verification page
      router.push(`/auth/verify-email?userId=${userId}`);
    },
  });
}

Try it

Staging only. Production API requires admin-managed captcha tokens and is rate-limited. Set NEXT_PUBLIC_OPENAPI_PROXY_URL env var to a staging proxy if you want browser-side requests; otherwise use the playground for request shape inspection only.

POST
/api/v1/auth/register

Request Body

application/json

TypeScript Definitions

Use the request body type in TypeScript.

Response Body

application/json

application/json

application/json

application/json

curl -X POST "https://loading/api/v1/auth/register" \  -H "Content-Type: application/json" \  -d '{    "email": "[email protected]",    "password": "SecureP@ss123",    "acceptedTerms": true,    "acceptedPrivacy": true  }'
{
  "success": true,
  "data": {
    "userId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "message": "Registration successful. Please check your email to verify your account."
  }
}
{
  "success": false,
  "error": {
    "code": "AUTH_UNAUTHORIZED",
    "message": "Invalid credentials",
    "i18nKey": "auth.login.invalid_credentials",
    "i18nVars": {
      "field": "email"
    },
    "details": [
      {
        "message": "email must be an email"
      }
    ],
    "correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
  }
}
{
  "success": false,
  "error": {
    "code": "AUTH_UNAUTHORIZED",
    "message": "Invalid credentials",
    "i18nKey": "auth.login.invalid_credentials",
    "i18nVars": {
      "field": "email"
    },
    "details": [
      {
        "message": "email must be an email"
      }
    ],
    "correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
  }
}
{
  "success": false,
  "error": {
    "code": "AUTH_UNAUTHORIZED",
    "message": "Invalid credentials",
    "i18nKey": "auth.login.invalid_credentials",
    "i18nVars": {
      "field": "email"
    },
    "details": [
      {
        "message": "email must be an email"
      }
    ],
    "correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
  }
}

Source

SourcePathLines
Controllerapps/api-core/src/modules/auth/auth.controller.ts60–96
DTO (request)apps/api-core/src/modules/auth/dto/index.ts5–65 (RegisterDto)
DTO (response)apps/api-core/src/modules/auth/dto/response.dto.ts6–12 (RegisterResponseDto)
Serviceapps/api-core/src/modules/auth/auth.service.ts126 (register(), ~120 lines)
Prisma modelpackages/prisma/prisma/schema.prisma24 (User)

On this page