npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@terreno/admin-spa

v0.22.2

Published

Opt-in admin SPA (Expo Router web app) plus an Express plugin that serves it from a Terreno backend

Readme

@terreno/admin-spa

Opt-in package that lets any Terreno backend serve a pre-built admin SPA from the same Node process — no separate static-site deploy to GCS/Netlify/Cloudflare Pages.

The default backend image stays small: React/Expo assets only ship in the npm tarball for consumers who actually register the plugin. The embedded admin use case (admin screens inside a consumer's main Expo app, as example-frontend/app/admin/* does) remains supported and unchanged.

What's here

  • Backend serving plugin (src/, AdminSpaServeApp): serves the pre-built Expo Router static export (dist/) and a runtime app-config.json from a Terreno backend.
  • The SPA frontend (app/, store/, components/): an Expo Router web app that wires @terreno/admin-frontend's screens with a Better-Auth session gate. It uses the @terreno/admin-frontend apiBase/routeBase prop split so API calls go to /admin while in-app navigation stays inside the SPA.

Boot flow: AppConfigGate fetches app-config.jsonStoreProvider builds the Better-Auth client + Redux store → AdminGate syncs the session and redirects anonymous users to /login and non-admins (admin API returns 403) to /forbidden.

Install

bun add @terreno/admin-spa

@terreno/api is a peer dependency.

Register with a backend

import {AdminSpaServeApp} from "@terreno/admin-spa";
import {AdminApp} from "@terreno/admin-backend";
import {BetterAuthApp, TerrenoApp} from "@terreno/api";

new TerrenoApp({userModel: User})
  .register(new BetterAuthApp({config: betterAuthConfig, userModel: User}))
  .register(new AdminApp({models: [...]}))
  .register(
    new AdminSpaServeApp({
      basePath: "/console", // default; non-breaking with the /admin API
      appConfig: {
        brandName: "Acme Admin",
        logoUrl: "/static/logo.svg",
        primaryColor: "#FF6B35",
        providers: ["email", "google"],
      },
    })
  )
  .start();

Open https://api.acme.com/console/ → admin UI. Same-origin → Better-Auth session cookies attach automatically, no CORS.

Options (AdminSpaServeOptions)

| Option | Default | Description | |---|---|---| | basePath | /console | Path the SPA mounts at. The /admin API is untouched. | | appConfig | see below | Runtime config served at ${basePath}/app-config.json. Merged over defaults. | | distDir | <pkg>/dist | Override the pre-built bundle directory (tests / custom builds). | | devProxyTarget | — | In dev, proxy all SPA paths to a running expo start --web, e.g. http://localhost:8083. |

App config

app-config.json lets a single pre-built bundle be themed and pointed at the right auth/admin API paths per consumer without rebuilding. Defaults:

{
  brandName: "Terreno Admin",
  primaryColor: "#2563EB",
  providers: ["email"],
  authBasePath: "/api/auth",
  adminApiBasePath: "/admin",
}

How serving works

  • ${basePath}/_expo and ${basePath}/assets are served with Cache-Control: public, max-age=31536000, immutable.
  • ${basePath}/app-config.json and index.html are served with Cache-Control: no-store.
  • index.html's absolute /_expo/ and /assets/ references are rewritten once at boot to ${basePath}/... (a no-op when the bundle was already built for that base).
  • The serve plugin injects window.__ADMIN_SPA_BASE__ = "${basePath}" into index.html so the SPA can resolve app-config.json on deep refreshes.
  • SPA fallback: both the bare ${basePath} and ${basePath}/*splat return index.html (Express 5 named-splat convention).

Mount path / baseUrl

Client-side routing under a sub-path requires the bundle to be built with a matching router base. app.json sets experiments.baseUrl: "/console", so the default build is served at /console and basePath must match. To mount elsewhere, rebuild with the matching base (e.g. EXPO_BASE_URL=/admin-ui bun run build:web) and set basePath accordingly. Mounting at the origin root (basePath: "/") needs no base.

Develop, build, and test

bun run compile      # compile the server plugin (src/ -> src/dist, CommonJS)
bun run build:web    # produce the static export in dist/
bun run dev          # expo start --web for local SPA development
bun run test:ci      # serve-plugin unit tests (supertest)
bun run smoke        # backend-free smoke over the built dist/
bun run test:e2e     # Playwright e2e (anonymous -> login) over the built dist/

dist/ is produced by bun run build:web and shipped via files: ["src/dist/**", "dist/**"]. The publish-on-tag CI job runs both the server compile and the web export before npm publish, so the published tarball's dist/ is populated.

Comparison with embedded admin-frontend

| | Standalone SPA (@terreno/admin-spa) | Embedded (@terreno/admin-frontend) | |---|---|---| | Deploy | Served by the API process at /console | Bundled into the consumer's Expo app | | Backend footprint | Opt-in; no React in default image | n/a | | Auth | Better-Auth same-origin cookies | Consumer's existing auth |