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 😎

Basic Auth

  • Category: Extra

On this page

  1. Mounting it
  2. The user table: users
  3. A custom check: validator
  4. Hooks: logging failures
  5. The responses
  6. challenge: false — suppress the browser dialog
  7. Brute-force throttling
  8. Advanced API
  9. Reference
  10. basicAuth(options)
  11. Behavior at a glance

@point0/basic-auth is an HTTP Basic auth gate. basicAuth(options) returns a Point0 middleware function: give it a user table, mount it on a point, and every request without valid username:password credentials gets a 401. It runs server-side only and adds per-user / per-IP brute-force throttling.

The most direct use is to close your whole site off — a staging deploy, a private preview, an internal tool. Mount it on root and the browser pops its native login dialog before anyone reaches a single page:

import { basicAuth } from '@point0/basic-auth'

export const root = Point0.lets
  .root()
  // ...
  // every request under root needs admin:secret first
  .middleware(basicAuth({ users: { admin: 'secret' } }))
  .root()

No page knows or cares it's there.

Mounting it

basicAuth(...) is a plain Point0 middleware, so it goes wherever .middleware goes. Three useful placements:

Guard everything on a point. Pass it straight to .middleware() and it gates every request reaching that point's scope — this is the whole-site gate from above:

export const root = Point0.lets
  .root()
  // guards every request under root
  .middleware(basicAuth({ users: { admin: 'secret' } }))
  .root()

Guard one path. .middleware also takes a route, so you can scope the gate to a subtree — e.g. an /admin/* area while the rest of the site stays open:

export const root = Point0.lets
  .root()
  .middleware('/admin/*', basicAuth({ users: { admin: 'secret' } }))
  .root()

Guard the OpenAPI docs. Pass it as the before option of openapi(...), which runs it only on the doc routes. This is how every shipped example uses it:

import { openapi } from '@point0/openapi'

// examples/basic/src/lib/root.tsx
export const root = Point0.lets
  .root()
  .middleware(
    openapi({
      route: '/openapi.json',
      scalar: '/scalar',
      swagger: '/swagger',
      filter: 'all',
      before: basicAuth({ users: { admin: 'admin' } }), // ← the gate
    }),
  )
  .root()

Now /openapi.json, /scalar, and /swagger prompt for a login; anything else stays open.

basicAuth is a no-op on the client — middleware runs server-side only, and the compiler strips server middleware bodies out of the client bundle.

The user table: users

users is the built-in credential check. It accepts three shapes, all normalized to a { username: password } record:

export const root = Point0.lets
  .root()
  .middleware(basicAuth({ users: { admin: 'secret', john: 'pass123' } }))
  .root()

The same pair also goes in as a single "user:pass" string (users: 'admin:secret') or as a list of them (users: ['admin:secret', 'john:pass123']).

Use the string form in production — keep the credentials in an env var, not in source. This is what create-point0-app scaffolds and what Start0 ships:

// packages/create-app/template/src/lib/root.tsx
export const root = Point0.lets
  .root()
  .middleware(
    openapi({
      route: '/openapi.json',
      before: basicAuth({ users: serverEnv.OPENAPI_CREDENTIALS }), // e.g. "admin:admin"
    }),
  )
  .root()

A "user:pass" string splits on the first :, so the password may itself contain colons. An empty username or empty password (':pass', 'user:', or a string with no :) throws at config time — when basicAuth(...) is called, not per request:

export const root = Point0.lets
  .root()
  // throws: Invalid user string format. Expected "username:password".
  .middleware(basicAuth({ users: 'admin:' }))
  .root()

Passwords are compared as plaintext with strict ===, guarded by hasOwnProperty (so prototype keys like toString can't be used as a login). There's no hashing — for hashed passwords or a database lookup, use validator below.

A custom check: validator

Pass a validator function instead of users to replace the built-in table entirely. It receives the parsed credentials plus the full request, returns a boolean, and may be async:

export const root = Point0.lets
  .root()
  .middleware(
    basicAuth({
      validator: async ({ username, password, request }) => {
        const user = await db.user.findUnique({ where: { username } })
        return !!user && (await verifyHash(password, user.passwordHash))
      },
    }),
  )
  .root()

users and validator are mutually exclusive — pass exactly one. The type is a discriminated union, so passing both (or neither) is a compile error.

Hooks: logging failures

Three optional callbacks fire on each failure path, before the failure response is built — for logging or metrics. They may be async (and are awaited) but can't change the response:

export const root = Point0.lets
  .root()
  .middleware(
    basicAuth({
      users: { admin: 'secret' },
      onUnauthorized: ({ ip }) => console.warn('no credentials', { ip }),
      onWrongCredentials: ({ username, ip }) =>
        console.warn('bad credentials', { username, ip }),
      onLimitExceeded: ({ username, ip }) =>
        console.error('throttled', { username, ip }),
    }),
  )
  .root()

onLimitExceeded also receives limitPerUser, limitPerIp, and staleTimeMs. All three receive { request, username, ip } (username/ip may be undefined).

The responses

The gate produces three outcomes, each a real HTTP response:

CaseStatusBodyWWW-Authenticate
no / malformed header401Unauthorizedsent (unless challenge: false)
wrong credentials401Unauthorizedsent (unless challenge: false)
too many failures429Too many failed HTTP auth attempts. Limit exceeded.never sent

The WWW-Authenticate header is the constant Basic realm="Restricted", charset="UTF-8" — it's what makes a browser pop its native login dialog. The realm is fixed to "Restricted" and the charset to UTF-8; there is no option to change either.

The Basic scheme is matched case-insensitively, so Basic, basic, and BASIC all parse.

challenge: false — suppress the browser dialog

Set challenge: false to drop the WWW-Authenticate header from 401s — useful for an API where you handle the 401 in your own client and don't want a browser prompt:

export const root = Point0.lets
  .root()
  // 401s carry no WWW-Authenticate
  .middleware(basicAuth({ users: { admin: 'secret' }, challenge: false }))
  .root()

The 429 never carries the challenge header, even with challenge: true.

Brute-force throttling

Every failed credential attempt (a valid Basic header with a wrong username/password) is recorded in memory as { dateMs, username, ip }. Requests with no or malformed header are not counted toward the throttle. Once a client crosses a limit, further attempts return 429 instead of 401:

export const root = Point0.lets
  .root()
  .middleware(
    basicAuth({
      users: { admin: 'secret' },
      limitPerUser: 100, //                 max failures per username (default 100)
      limitPerIp: 100, //                   max failures per IP       (default 100)
      staleTimeMs: 1000 * 60 * 60 * 24, //  failures older than this are forgotten (default 24h)
      memorySize: 1000, //                  hard cap on remembered attempts (default 1000)
    }),
  )
  .root()

The limit trips when either the per-user or the per-IP count is reached (they're OR-combined). A successful login clears every recorded failure matching that IP or that username — so a login from one user can also wipe another user's recorded failures that share the same IP. The IP is request.from.clientIp — the visitor's address even behind a proxy/CDN, where the raw peer would be the same internal hop for everyone and the per-IP limit would collapse into one global bucket — falling back to the unspoofable request.from.ip; requests with no resolvable IP all share one 'unknown' bucket.

Gotcha — the memory is in-process and volatile. It resets on restart and is per-instance. Across multiple server processes, the limit counts per process, not globally. There's no shared / persistent store option.

Advanced API

basicAuth(options) is the high-level factory. The package also exposes a lower-level surface:

  • BasicAuth.create(options) — the class behind the factory (the constructor is private). Its .middleware getter is exactly what basicAuth() returns. validateRequest and getFailureResponse are methods on a BasicAuth instance, not separate top-level exports.
  • instance.validateRequest(request) — returns the full BasicAuthValidationResult ({ ok, username, ip, response, reason } on failure) instead of acting as middleware.
  • instance.getFailureResponse(request) — returns the failure Response, or undefined when the request is authorized. For gating by hand, outside .middleware().
  • getBasicAuthHeader(username, password) — a top-level export that builds a Basic <base64> header value, for crafting authenticated requests in tests.

Reference

basicAuth(options)

Exactly one of users / validator is required; everything else is optional.

OptionTypeDefaultWhat
usersRecord<user,pass> | "user:pass" | string[]—built-in credential table (mutually exclusive with validator)
validator({ username, password, request }) => boolean | Promise—custom check, replaces users
challengebooleantruesend WWW-Authenticate on 401 (browser login dialog)
limitPerUsernumber100max failed attempts per username before 429
limitPerIpnumber100max failed attempts per IP before 429
staleTimeMsnumber86_400_000how long a failed attempt is remembered (24h)
memorySizenumber1000hard cap on remembered attempts
onUnauthorized({ request, username, ip }) => void—hook: no / bad header
onWrongCredentials({ request, username, ip }) => void—hook: wrong credentials
onLimitExceeded({ request, username, ip, limitPerUser, limitPerIp, staleTimeMs }) => void—hook: throttled

Behavior at a glance

AspectBehavior
Sideserver-only — a no-op on the client
SchemeBasic matched case-insensitively
Password with :allowed — "user:pass" splits on the first : only
Empty user / passthrows at config time, not per request
Password comparisonplaintext ===, hasOwnProperty-guarded — no hashing
Realm / charsetfixed Basic realm="Restricted", charset="UTF-8" — not configurable
Throttle limitsper-user OR per-IP; in-memory, volatile, per-process
Missing IPbucketed under 'unknown'
PreviousCookieStoreNextCORS

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