QUESTPIE
ConceptsFields

URL field

f.url() is a string field for web addresses, a varchar(2048) with automatic URL-format validation and host/protocol-aware filter operators.

f.url() stores a web address. It produces a varchar(2048) column, derives a Zod schema that enforces a valid URL (z.string().url()), and exposes the string filter operators plus URL-aware ones (host, protocol) on the typed where clause. Reach for it whenever a field holds a link, a website, a webhook target, a canonical URL, and you want the format checked for free.

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

What it does

  • Stores a URL string, a varchar(2048) column by default (the larger default size accommodates long query strings), resizable via the constructor argument.
  • Validates the format automatically, the derived schema is z.string().url(), so a malformed value is rejected at validation time. No .url() call needed.
  • Filters with URL-aware operators, every string operator (eq, contains, ilike, …) plus host, hostIn, and protocol for matching the host or scheme of a URL.
  • Renders a URL input in the admin form.

Quick start

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

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

export const links = collection("links").fields(({ f }) => ({
  website: f.url().required(),   // varchar(2048), NOT NULL, validated as a URL
  webhook: f.url(500),           // varchar(500), still URL-validated
}));

That gives links a website column typed string on read, required on insert, rejected unless it parses as a URL, and filterable with the operators below, no schema or migration written by hand.

Constructor argument

f.url() takes one optional argument: the column's max character length.

CallColumnNotes
f.url()varchar(2048)Default. maxLength defaults to 2048, wider than f.text()'s 255, since URLs run long.
f.url(n)varchar(n)Sized varchar; n is also applied as a Zod .max(n).

The resulting field data is always string. The derived Zod schema is z.string().url().max(maxLength), the .url() refinement is added unconditionally, so every value is validated as a URL regardless of the length you pass.

Chained methods

These methods are specific to url (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 (Zod .min(n)).
.max(n)Maximum string length (Zod .max(n)).
collection("integrations").fields(({ f }) => ({
  endpoint: f
    .url()
    .required()
    .min(12), // reject trivially short strings, in addition to URL-format validation
}));

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

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

URL validation is always on; tighten it with `.zod()`

The .url() refinement is appended automatically (derive-schema.ts:131), you don't add it and you can't turn it off through a field method. To go further (e.g. allow only https, or restrict hosts), use the .zod() escape hatch: f.url().zod((s) => s.refine((v) => v.startsWith("https://"))).

Filtering, operators

url uses the URL operator set (urlOps), which is the string operator set extended with three URL-specific operators. Every url field is filterable with these in a where clause.

The inherited string operators (operand string, or string[] for in / notIn):

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%').
startsWith / endsWithstringPrefix / suffix match.
isNull / isNotNullbooleanColumn is (not) null.

The URL-specific additions:

OperatorOperandMatches
hoststringRows whose URL contains ://value (case-insensitive), i.e. the given host. e.g. "example.com".
hostInstring[]Rows matching any host in the list.
protocolstringRows whose URL begins value:// (case-sensitive), i.e. the given scheme. e.g. "https".
// All links pointing at a given host
const { docs } = await app.collections.links.find({
  where: { website: { host: "questpie.com" } },
});

// Only secure (https) endpoints
const secure = await app.collections.links.find({
  where: { webhook: { protocol: "https" } },
});

`host`/`protocol` are pattern matches, not a parsed URL

These operators match against the raw stored string with LIKE/ILIKE, they don't parse the URL. host looks for ://value anywhere in the string (so it also matches a host that appears inside a path), and protocol is a case-sensitive prefix match on value://. They're convenient filters, not a strict URL parser.

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.url(), any field that holds a web address you want format-checked: a site URL, a webhook endpoint, a canonical link, an avatar URL, an external profile.
  • For a plain string with no URL validation (a path, a slug, an arbitrary identifier), use f.text().
  • For an email address, use f.email(), the same string shape with z.string().email() validation and domain operators instead (see Email field).
  • To upload a file and store a link to it, reach for the Upload field (f.upload()), or the collection-level .upload() builder, not a url field.

Multiple values

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

links: f.url().array().maxItems(5), // string[] of URLs stored as jsonb

Each element is still validated as a URL.

TypeScript

A url 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 Link = typeof links.$infer.select;
//   ^? { id: string; website: string; webhook: 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 plain-string sibling and the stringOps operator set url extends.
  • Validation, the auto-derived Zod schema and the .zod() escape hatch for stricter URL rules.

On this page