Point0by 1gr14
  • Меню
    • GitHub
    • Блог
  • Introduction
    • Overview
    • Getting Started
    • Full Overview
    • Benchmarks
    • Points
  • Points
    • Page
    • Layout
    • Component
    • Provider
    • Mountable
    • Query
    • Infinite Query
    • Mutation
    • Action
    • Root
    • Base
    • Plugin
  • Methods
    • Validation
    • Loader
    • Context
    • Middleware
    • Loading & error
    • .with
    • Mapper
    • Transformer
    • Stage Methods
  • Core
    • Navigation
    • SSR
    • RSC
    • 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
    • Capacitor
    • Expo
  • 1gr14
    • Главная
    • Start0
    • Поддержать
    • Обучение
    • Группа
    • Блог
    • Автор
  • Сообщество
    • Discord
    • Telegram
  • Опенсорс
    • Point0
    • Route0
    • Error0
    • Flat
  • Аккаунт
    • Войти
    • Регистрация
Point0by 1gr14
Создаю опенсорс во славу Господа Иисуса Христа ☦️
С любовью ко всем разработчикам на свете ❤️
Условия использованияПолитика конфиденциальностиСергей Дмитриев 2026 😎

Basic Auth

  • Раздел: 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:

// guards every request under root
.middleware(basicAuth({ users: { admin: 'secret' } }))

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:

.middleware('/admin/*', basicAuth({ users: { admin: 'secret' } }))

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
.middleware(
  openapi({
    route: '/openapi.json',
    scalar: '/scalar',
    swagger: '/swagger',
    filter: 'all',
    before: basicAuth({ users: { admin: 'admin' } }), // ← the gate
  }),
)

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:

basicAuth({ users: { admin: 'secret', john: 'pass123' } }) // record
basicAuth({ users: 'admin:secret' }) //                       single "user:pass" string
basicAuth({ users: ['admin:secret', 'john:pass123'] }) //     list of "user:pass" strings

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
before: basicAuth({ users: serverEnv.OPENAPI_CREDENTIALS }) // e.g. "admin:admin"

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:

basicAuth({ users: 'admin:' }) // throws: Invalid user string format. Expected "username:password".

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:

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

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:

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 }),
})

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:

basicAuth({ users: { admin: 'secret' }, challenge: false }) // 401s carry no WWW-Authenticate

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:

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)
})

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 comes from request.from.ip (Bun's trusted source first, then x-forwarded-for / x-real-ip / cf-connecting-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'
НазадCookieStoreДалееCORS

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