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
- User registers (basic account).
- On first sensitive action, redirect to verification flow.
- Provider verifies ID document and matches selfie.
- Webhook back: user verified.
- Set
traits.age_verified: truein Kratos. - 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.
Parental consent (COPPA, US)
If you allow under-13 users with parental consent:
- Child enters birthday → under 13 detected.
- Ask for parent's email.
- Send verification email to parent.
- Parent clicks → CAPTCHA + acknowledgment → account activated.
- Store
traits.parent_consent_verified: trueandparent_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
| Jurisdiction | Minimum 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.