Olympus Docs
CookbookAuth flows

Age verification at signup

Refuse signup for users under a minimum age

If your service has age restrictions (typically 16 for GDPR, 13 for COPPA, 18 for some financial / health), you need to gate signup.

Approach 1: Self-declared birthday (low assurance)

Simplest. Ask date of birth at registration, reject if under threshold.

Add the trait

// identity.schema.json
"birthday": {
  "type": "string",
  "format": "date",
  "title": "Date of birth"
}

Validate in pre-registration hook

export async function POST(req: Request) {
  const { traits } = await req.json();
  const dob = new Date(traits.birthday);
  const age = (Date.now() - dob.getTime()) / (1000 * 60 * 60 * 24 * 365.25);
  if (age < 16) {
    return Response.json({
      reject: true,
      error: "age_restriction",
      message: "You must be at least 16 to use this service.",
    });
  }
  return Response.json({ ok: true });
}

Limitations

  • Trivially bypassed (user lies).
  • "Soft" compliance, for many services this is sufficient (the regulation requires "reasonable effort," not certainty).

Approach 2: Government ID check (high assurance)

For services with strict requirements (alcohol, gambling, healthcare), partner with an ID verification provider:

Flow

  1. User registers (basic account).
  2. On first sensitive action, redirect to verification flow.
  3. Provider verifies ID document and matches selfie.
  4. Webhook back: user verified.
  5. Set traits.age_verified: true in Kratos.
  6. Authz uses this trait.

Webhook handler

// On verification provider webhook
export async function POST(req: Request) {
  const { user_id, verified, date_of_birth } = await req.json();
  if (!verified) return new Response("OK");
  await kratos.adminPatchIdentity(user_id, [
    { op: "replace", path: "/traits/age_verified", value: true },
    { op: "replace", path: "/traits/birthday", value: date_of_birth },
  ]);
  return new Response("OK");
}

Cost

USD 1-3 per verification. Build into your unit economics or charge for premium tiers.

Approach 3: Payment method as proxy

Credit cards effectively confirm 18+ in most jurisdictions. Stripe payment method on file = "user is adult." Low assurance compared to ID check; common for adult-content sites.

If you allow under-13 users with parental consent:

  1. Child enters birthday → under 13 detected.
  2. Ask for parent's email.
  3. Send verification email to parent.
  4. Parent clicks → CAPTCHA + acknowledgment → account activated.
  5. Store traits.parent_consent_verified: true and parent_email.

For higher assurance: parent must enter credit card (charge $0.01 then refund) to verify.

Don't store full DOB unless you must

DOB is highly identifying. If you only need age-at-signup:

// Store year only:
const yearOfBirth = new Date(traits.birthday).getFullYear();
await kratos.adminPatchIdentity(id, [
  { op: "replace", path: "/traits/year_of_birth", value: yearOfBirth },
  { op: "remove", path: "/traits/birthday" },
]);

Less PII to leak.

Age-restricted features post-signup

// In your app:
function canAccessFeature(user: User, feature: string) {
  if (feature === "alcohol" && !user.traits.age_verified) return false;
  if (feature === "gambling" && getAge(user.traits.birthday) < 21) return false;
  return true;
}

Regulatory baseline

JurisdictionMinimum age (without parental consent)
US (COPPA)13
EU (GDPR)16 (some states lower to 13)
UK (post-Brexit)13
Brazil (LGPD)18 (12 with parental consent)
Canada (PIPEDA)Capacity-based, no firm number

Pick the highest applicable to your user base, usually 16 for global services.

On this page