Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.pylonsync.com/llms.txt

Use this file to discover all available pages before exploring further.

After a user signs up (via password, OAuth-with-unverified-email, etc.), you usually want to confirm they own the email address. Pylon’s email-verification flow is the same primitive as magic-code sign-in, gated on an authenticated session — it sends a 6-digit code to the user’s email field and stamps emailVerified on successful verify. The OAuth + SSO sign-in paths automatically stamp emailVerified because the upstream IdP just vouched for the email. Use this flow when the email wasn’t IdP-verified — manual signup, edit-email flow, etc.

Endpoints

EndpointMethodAuthPurpose
/api/auth/email/send-verificationPOSTsessionEmail a 6-digit code to the user’s current email
/api/auth/email/verifyPOSTsessionSubmit the code; stamp emailVerified

Schema

The User entity needs an emailVerified field. Pylon writes the current ISO 8601 timestamp on success:
import { entity, field } from "@pylonsync/sdk";

export const User = entity("User", {
  email: field.string().unique(),
  emailVerified: field.string().optional(),  // ISO 8601 — null = not verified
  // ...
});
If your schema lacks this field, update will reject the unknown column and /email/verify returns a 4xx with the storage layer’s error code instead of silently succeeding.

Sending a verification code

curl -X POST https://your-app/api/auth/email/send-verification \
  -H 'Authorization: Bearer pylon_token'
Pylon looks up the caller’s User row, reads email, mints a 6-digit code, and sends an email with subject "Verify your email address" via the configured email transport. Body: "Your email verification code is: <code>\n\nThis code will expire in 10 minutes.". Response in production:
{ "sent": true, "email": "alice@acme.com" }
Response in dev (PYLON_DEV_MODE=true):
{ "sent": true, "email": "alice@acme.com", "dev_code": "123456" }
Errors:
StatusCodeReason
401UNAUTHORIZEDNo session
404USER_NOT_FOUNDSession resolves to a user_id with no matching row
400MISSING_EMAILUser row has no email field
429RATE_LIMITEDA code was requested within the throttle window
500EMAIL_SEND_FAILEDEmail transport returned an error
The throttle is shared with magic-code sends — 1 code per email per minute. The 10-minute TTL is shared too.

Verifying

curl -X POST https://your-app/api/auth/email/verify \
  -H 'Authorization: Bearer pylon_token' \
  -H 'Content-Type: application/json' \
  -d '{"code": "123456"}'
Response on success:
{ "verified": true }
Pylon stamps emailVerified to the current ISO 8601 timestamp (e.g. "2026-01-15T10:30:00Z"). Errors:
StatusCodeReason
401UNAUTHORIZEDNo session
400MISSING_CODEcode field absent
400INVALID_JSONBody wasn’t JSON
4xxINVALID_CODEWrong code, expired, or burned

Gating handlers on emailVerified

In your TS code:
import { action } from "@pylonsync/functions";

export default action({
  async handler(ctx, args) {
    if (!ctx.auth.userId) throw new Error("sign in");
    const user = await ctx.db.get("User", ctx.auth.userId);
    if (!user?.emailVerified) {
      throw ctx.error("EMAIL_NOT_VERIFIED", "Verify your email before posting");
    }
    // proceed
  },
});
In policies, project emailVerified from the User entity and reference it directly:
import { policy } from "@pylonsync/sdk";

export const postsPolicy = policy({
  entity: "Post",
  // Pull the related user's emailVerified through the relation.
  allowInsert: "auth.userId == data.authorId",
  allowRead: "true",
});
Pylon doesn’t bake an opinionated auth.emailVerified into the policy DSL today — the field lives on your User row and you reference it via your own handlers / queries.

Security guarantees

  • Code generation, comparison, throttling shared with magic-codes — 6-digit numeric, 10-minute TTL, burn-after-5-wrong-attempts, constant-time comparison.
  • Session-gated — only the authenticated user can request + verify their own email. There’s no admin override here; if you need to mark a verified-via-out-of-band-process, write the User row directly with admin auth.
  • ISO 8601 timestamp formatpylon_kernel::util::now_iso() produces 2026-01-15T10:30:00Z. The previous <unix>Z format was silently rejected by the storage adapter, leaving users in a perpetual unverified state. Fixed in framework — flag emails are verified iff emailVerified is non-null.

Where to go next

  • Magic codes — the same 6-digit code primitive as a primary sign-in flow
  • Password — the register flow that produces an unverified email