QUESTPIE
ConceptsFields

Email field

f.email() is a string field backed by a Postgres varchar that validates the value as an email address and adds domain-aware filter operators on top of the string set.

f.email() stores an email address. It produces a varchar(255) column, derives a Zod schema that enforces a valid email format and the column length, and exposes the full string operator set plus email-specific domain / domainIn filters on the typed where clause. Reach for it instead of f.text() whenever a field must hold a valid email, you get the format check and the domain filters for free.

Prerequisites: read Fields first, this page covers only the email type. The f proxy, the chain-modifier model, and shared modifiers like .required() / .localized() / .default() are taught there.

What it does

  • Stores an email string, a varchar(255) column by default; pass a number to resize the column.
  • Validates the format, the derived Zod schema is z.string().email().max(maxLength), so invalid addresses are rejected on create and update without writing a validator.
  • Filters by domain, on top of the string operators, email adds domain and domainIn to match the part after @ (case-insensitive).
  • Filters with string operators, eq, ne, in, contains, ilike, startsWith, isNull, and the rest, all typed against string.
  • Renders a text input in the admin form, typed as an email field.

Quick start

Use f.email() inside a .fields() callback. The positional argument is the max character length; chain modifiers to refine it.

src/questpie/server/collections/users.ts
import { collection } from "#questpie/factories";

export const users = collection("users").fields(({ f }) => ({
  email: f.email().required(),          // varchar(255), NOT NULL, validated
  backupEmail: f.email(320),            // sized varchar(320), optional
}));

That gives users an email column typed string on read, required on insert, validated as a well-formed address at ≤ 255 chars, and filterable by domain, no schema or migration written by hand.

Constructor argument

f.email() takes a single optional maxLength.

CallColumnNotes
f.email()varchar(255)Default. maxLength defaults to 255.
f.email(n)varchar(n)Sized varchar; n is also applied as a Zod .max(n).

The field data is always string. The derived Zod schema is z.string().email().max(maxLength), the .email() refinement is always present, and the .max() uses the 255 default or your n.

Need a longer column? 320 is the practical email max

The default varchar(255) holds nearly every real address, but the RFC upper bound is 320 characters. If you must accept the longest legal addresses, pass f.email(320).

Chained methods

These methods are specific to email (on top of the shared modifiers every field has). Each returns a new immutable field, so they chain in any order.

MethodEffect
.min(n)Minimum string length (maps to minLength).
.max(n)Maximum string length (maps to maxLength).
collection("contacts").fields(({ f }) => ({
  email: f.email().required().max(254), // valid email, capped at 254 chars
}));

`.min()` / `.max()` here are STRING LENGTH, not value bounds

On an email field, .min(n) / .max(n) constrain the number of characters (they map to minLength / maxLength, the same as f.text()). On f.number() the same method names constrain the numeric value. Same names, different meaning, sized by the field's type.

Filtering, operators

email uses the email operator set (emailOps), which extends the string set with two domain matchers. So every email field is filterable with all the string operators and domain / domainIn. The operand is typed string (or string[] for the list operators).

OperatorOperandMatches
eq / nestringExact equal / not equal.
in / notInstring[]Value is (not) in the list.
like / notLikestringSQL LIKE (case-sensitive, your own %).
ilike / notIlikestringCase-insensitive LIKE.
containsstringSubstring (LIKE '%value%').
startsWithstringPrefix (LIKE 'value%').
endsWithstringSuffix (LIKE '%value').
isNull / isNotNullbooleanColumn is (not) null.
domainstringEmail's domain matches (case-insensitive ILIKE '%@value').
domainInstring[]Email's domain is one of the listed domains.
// Everyone on the acme.com domain
const { docs } = await app.collections.users.find({
  where: { email: { domain: "acme.com" } },
});

// Internal staff: any of two domains
const staff = await app.collections.users.find({
  where: { email: { domainIn: ["acme.com", "acme.io"] } },
});

`domain` matches the suffix after `@`, not a substring

domain compiles to ILIKE '%@value', so { domain: "acme.com" } matches a@acme.com but not a@mail.acme.com (the stored domain is mail.acme.com, not acme.com). For broader matching use a string operator, e.g. { ilike: "%@%acme.com" }.

For the full query language, combining field filters with AND / OR / NOT, pagination, and orderBy, see Fields → filtering. Relation filters and hydration are covered in Relations.

When to use it

  • f.email(), any field that must hold a valid email address: a user's login email, a contact address, a notification recipient. You get format validation and domain filters that a plain string field lacks.
  • For a free-form short string with no format constraint, use f.text() instead.
  • For a website or link, use f.url(), the same idea (a validated string), but with z.string().url() and host/protocol filter operators.

Format-valid is not the same as deliverable

z.string().email() checks syntax only. It does not verify the mailbox exists or the domain accepts mail. Treat a passing value as well-formed, not reachable.

Multiple values

Chain .array() to store a list of email strings in a single jsonb column, and bound it with .minItems(n) / .maxItems(n). This is a shared modifier, not email-specific:

ccList: f.email().array().maxItems(20), // string[] of validated emails, stored as jsonb

TypeScript

An email field contributes a string to the generated row, insert, and where types, no annotation needed. After questpie generate, pull the shapes off the collection or the app types:

type User = typeof users.$infer.select;
//   ^? { id: string; email: string; backupEmail: string | null; ... }

.required() makes the field non-null and required on insert; without it the column is nullable and the insert field optional. See Fields → inferred types for how modifiers flow into the generated types.

  • Fields, the f proxy, the chain-modifier model, and the shared modifiers (.required(), .localized(), .default(), .array(), …).
  • Text field, the unvalidated single-line string sibling.
  • Validation, the auto-derived Zod schema and the .zod() escape hatch for custom rules.

On this page