Point0by 1gr14
  • Menu
    • GitHub
    • Blog
  • Introduction
    • Overview
    • Getting Started
    • Full Overview
    • Benchmarks
    • Points
  • Points
    • Page
    • Layout
    • Component
    • Provider
    • Mountable
    • Query
    • Infinite Query
    • Mutation
    • Action
    • Subscription
    • Root
    • Base
    • Plugin
  • Methods
    • Validation
    • Loader
    • Context
    • Middleware
    • Loading & error
    • .with
    • Mapper
    • Transformer
    • Stage Methods
  • Core
    • Navigation
    • SSR
    • RSC
    • Socket
    • Request
    • Response
    • Error handling
    • Env
    • Head
    • MDX
    • Assets
    • File upload
    • OpenAPI
    • Query client
    • Events
    • Infer
  • Engine
    • Engine Config
    • Engine Runtime
    • CLI
    • Dev
    • Build
    • Compiler
    • Generator
    • Project MCP
    • Docs MCP
    • Importer
    • Public dir
    • Testing
    • Deploy
    • Bun or Vite
  • Extra
    • SsrStore
    • CookieStore
    • Basic Auth
    • CORS
    • Cache-Control
    • Compress
  • Examples
    • Basic
    • Vite
    • Better Auth
    • Socket
    • Capacitor
    • Expo
  • 1gr14
    • Home
    • Start0
    • Support
    • Education
    • Group
    • Blog
    • Author
  • Community
    • Discord
    • Telegram
  • Open Source
    • Point0
    • Route0
    • Error0
    • Flat
    • Agents Party
  • Account
    • Sign In
    • Sign Up
Point0by 1gr14
Building open-source software for the glory of the Lord Jesus Christ ☦️
With love for developers of all backgrounds around the world ❤️
Terms of ServicePrivacy PolicySergei Dmitriev 2026 😎

CORS

  • Category: Extra

On this page

  1. Mounting it
  2. Locking down the origin: origin
  3. Preflight: preflight
  4. Methods and headers
  5. Credentials and caching
  6. Reference
  7. cors(options)
  8. Behavior at a glance

@point0/cors is a CORS middleware. cors(options) returns a Point0 middleware function: mount it on your root and every response gets the Access-Control-* headers, and every preflight OPTIONS request gets a 204 — so a client on a different origin (a native/mobile app, or a front-end served from another domain) can call your API.

import { Point0 } from '@point0/core'
import { cors } from '@point0/cors'

// examples/expo/src/lib/root.tsx — let the native app reach the API
export const root = Point0.lets
  .root()
  .middleware(cors()) // ← every response carries CORS headers; OPTIONS → 204
  // ...
  .root()

That bare cors() reflects whatever Origin the request carried, mirrors the request method, and echoes the requested headers back — but it sends no Access-Control-Allow-Credentials, so the browser makes those cross-origin calls anonymously. That is the safe half of permissive: reflecting an origin and allowing credentials is the one combination that lets any site on the internet read your authenticated responses with the visitor's own cookies. You opt into credentials deliberately, together with an origin — see Credentials and caching. The shipped expo example uses bare cors() because the Expo app and the API server live on different origins and it authenticates nothing by cookie.

cors() is cut from the client bundle — its body and the imports it uses are removed, so it never ships to the browser. .middleware(...) arguments are server-only, and the compiler prunes the then-unused @point0/cors import with them.

Mounting it

cors(...) is a plain Point0 middleware, so it goes wherever .middleware goes. On the root it covers the whole API, which is the usual placement:

export const root = Point0.lets
  .root()
  .middleware(cors({ origin: 'https://app.example.com' }))
  .root()

.middleware also takes a route, so you can scope CORS to a subtree:

export const root = Point0.lets.root().middleware('/api/*', cors()).root()

Locking down the origin: origin

origin decides which request origins are allowed. It defaults to true.

export const root = Point0.lets
  .root()
  .middleware(cors({ origin: 'https://app.example.com' }))
  .root()

It takes true (any origin), false (none), one origin string, a RegExp, a predicate, or an array mixing them. How each form resolves:

  • true — reflects the request's Origin header back. With no Origin header it falls back to *. Anonymous-only unless you also pass credentials: true, which spells out that you mean any origin with cookies.
  • false — never sets Access-Control-Allow-Origin; no cross-origin client is allowed.
  • string — compared against the request origin. A bare host like 'example.com' matches http://example.com and https://example.com (the protocol is ignored). Include a protocol — 'https://example.com' — and the protocol must match too. The match is exact on the host, not a substring: 'example.com' does not match notexample.com.
  • RegExp — .test(requestOrigin); the full origin string (with protocol) is tested.
  • function — receives the middleware context and returns boolean (may be async). Return true to allow the request's origin.
  • array — any mix of the above; the first entry that allows the origin wins.

When a request origin is allowed, that exact origin is echoed back in Access-Control-Allow-Origin (not a literal *), which is what browsers require once credentials are in play.

Preflight: preflight

A cross-origin request that isn't "simple" makes the browser send an OPTIONS preflight first. By default (preflight: true) cors() answers it with a bare 204 carrying the CORS headers — the real request then follows. Set preflight: false to let OPTIONS fall through to your own handler untouched (no CORS headers added):

export const root = Point0.lets
  .root()
  // OPTIONS is passed through to next(), no CORS headers
  .middleware(cors({ preflight: false }))
  .root()

Methods and headers

export const root = Point0.lets
  .root()
  .middleware(
    cors({
      methods: ['GET', 'POST'], //                           Access-Control-Allow-Methods
      allowedHeaders: ['Content-Type', 'Authorization'], //  Access-Control-Allow-Headers
      exposeHeaders: ['X-Total-Count'], //                   Access-Control-Expose-Headers
    }),
  )
  .root()

Each of methods, allowedHeaders, and exposeHeaders takes a string, a string array (joined with , ), or a boolean. They default to true, which means "reflect what the request asked for":

  • methods: true — mirrors the incoming request's method (or the Access-Control-Request-Method on a preflight). '*' and a single method like 'GET' are also accepted; false omits the header.
  • allowedHeaders: true — echoes the Access-Control-Request-Headers the preflight asked for (falling back to the keys present on the request).
  • exposeHeaders: true — exposes the keys present on the request.

Credentials and caching

export const root = Point0.lets
  .root()
  .middleware(
    cors({
      origin: 'https://app.example.com', // required once credentials are on
      credentials: true, //                Access-Control-Allow-Credentials: true
      maxAge: 600, //                      Access-Control-Max-Age in seconds
    }),
  )
  .root()
  • credentials defaults to false. Set it to true to send Access-Control-Allow-Credentials: true, which is what lets a cross-origin browser client send cookies (and read the response) — pair it with .fetchOptions({ credentials: 'include' }) on the client side.
  • credentials: true requires an explicit origin, and cors() throws without one. A reflected origin plus credentials means any page the visitor opens can fetch your API with their session cookie and read the body — account data, a socket connect ticket, anything the session reaches. Name the origins you trust: cors({ origin: 'https://app.example.com', credentials: true }). Writing cors({ origin: true, credentials: true }) still works and still reflects everything — that spelling is you saying you mean it (a closed network, a dev machine), not a default you can walk into.
  • When the origin resolves to * — origin: true and a request with no Origin header — the credentials header is dropped: browsers reject that pair outright, and there is no origin to echo instead.
  • maxAge is the preflight cache lifetime in seconds, written to Access-Control-Max-Age. It defaults to 5. A maxAge of 0 omits the header entirely.

Reference

cors(options)

Every option is optional. cors() with no argument allows any origin anonymously; credentials are opt-in and require an origin.

OptionTypeDefaultWhat
originboolean | string | RegExp | fn | array of thosetruewhich origins are allowed (true reflects the request origin)
methodsboolean | '*' | method | method[]trueAccess-Control-Allow-Methods (true mirrors the request)
allowedHeaderstrue | string | string[]trueAccess-Control-Allow-Headers (true reflects the request)
exposeHeaderstrue | string | string[]trueAccess-Control-Expose-Headers (true reflects the request)
credentialsbooleanfalsesend Access-Control-Allow-Credentials: true (needs an explicit origin, else throws)
maxAgenumber5Access-Control-Max-Age in seconds (0 omits the header)
preflightbooleantrueanswer OPTIONS preflight with 204 (else pass through)

Behavior at a glance

AspectBehavior
Sidecut from the client bundle — the .middleware(...) argument and its imports are removed, so it never ships to the browser
Default originreflects the request Origin; * only when there's no Origin header
Default credentialsoff — a reflected origin never carries credentials unless you name an origin yourself
Credentials + originan allowed origin is echoed literally; next to a * the credentials header is dropped (browsers reject that pair)
String origin matchhost-only by default; add a protocol to require a protocol match
Preflight OPTIONSanswered with 204 when preflight: true; passed through otherwise
Varyset to Origin for a reflected origin, * for a wildcard

If you also expose OpenAPI docs from a separate origin, the same cors() on the root covers those routes too.

PreviousBasic AuthNextCache-Control

Enjoying Point0?

Give it a star on GitHub, ask questions, and follow what's new

Star Point0

A star on GitHub helps more developers find Point0
Star on GitHub

Start0

Point0 integrated into the Start0 SaaS boilerplate — the best way to use it
Explore Start0

YouTube

Tutorials, walkthroughs, and updates
YouTube

Discord

Questions and chat in English
Join Discord

Telegram

News channel and chat in Russian
ChannelChat

Twitter

Posts and updates
Twitter