Admin

Consuming the base layer (versioned)

The canonical way a project admin depends on the base layer — a pinned versioned dependency with a local dev override. Use this for every new admin project.

Consuming the base layer (versioned)

This is the recommended way for a new admin project to depend on the base layer — as a pinned, versioned dependency (Mode B), with a gitignored local override for day-to-day development. It mirrors how Redelay Go apps consume the framework via versioned go get + a go.work dev override.

The older approach — a filesystem extends pointing at a sibling clone (shown in Layered architecture and Building a custom admin project) — is Mode A: fine for hacking inside the monorepo, but it means a fresh clone or CI can't build without the base-layer tree checked out next to it, and there's no version pin. Prefer the versioned approach below.

Reference implementation: fitniac/admin (the GymTracer admin). goshop-pl's admin is still Mode A and will be reworked to match this.

Naming: package vs. repo

ValueWhere it appears
npm package name@redelay/js-adminthe dependency key, node_modules/@redelay/js-admin, the pnpm override, the extends path, Tailwind @source
github reporedelay/js-admin-nuxt4the github: URL only

The two intentionally differ: the package was rebranded to the scoped @redelay/js-admin (branded, collision-safe, not locked to a Nuxt version) while the repo URL was kept to avoid breaking existing clones. Convention for future JS framework packages: scope them under @redelay/….

1. Pin the dependency

In the project admin's package.json, depend on the base layer by its package name, resolved from the repo at a tag:

json
{
  "dependencies": {
    "@redelay/js-admin": "github:redelay/js-admin-nuxt4#v0.4.29"
  }
}

Fetching a private repo needs GOPRIVATE-style git access — a git insteadOf ssh rewrite (host dev) or a token (CI). The dev container never fetches it (see the override below), so it needs no credentials.

2. Local dev override (the go.work analog)

A gitignored pnpm-workspace.yaml makes pnpm link the sibling checkout instead of fetching the pinned tag — so you edit base-layer source and HMR reflects it, with no version bump and no credentials in the container. pnpm 10 reads overrides from this file, and a link: override replaces the spec before git resolution (it never touches the network):

yaml
# pnpm-workspace.yaml  — gitignored; commit pnpm-workspace.yaml.example instead
overrides:
  "@redelay/js-admin": "link:../../redelay/js-admin-nuxt4"

Commit a pnpm-workspace.yaml.example template and gitignore the real file, so a fresh clone / CI (which has no override) installs the pinned tag and a make target seeds the override for local dev:

make
workspace:
    @test -f pnpm-workspace.yaml || cp pnpm-workspace.yaml.example pnpm-workspace.yaml

The committed pnpm-lock.yaml pins the git version (authoritative). A local install with the override rewrites it to the link: form — don't commit that churn; have the dev container install with --no-lockfile so it doesn't write the lockfile back through the bind mount.

3. nuxt.config — extend by REAL path

ts
import { realpathSync } from 'fs'
import { resolve, dirname } from 'path'
import { fileURLToPath } from 'url'
const __dirname = dirname(fileURLToPath(import.meta.url))

const baseLayer = realpathSync(resolve(__dirname, 'node_modules/@redelay/js-admin'))

export default defineNuxtConfig({
  extends: [baseLayer],
  // …
})

Two subtleties this line handles — see the gotchas below.

4. Tailwind @source

Point the globs at the package under node_modules (works in both dev — a symlink — and versioned/CI — the real package):

css
@source "../../node_modules/@redelay/js-admin/components/**/*";
@source "../../node_modules/@redelay/js-admin/layouts/**/*";
@source "../../node_modules/@redelay/js-admin/pages/**/*";
@source "../../node_modules/@redelay/js-admin/modules/**/*";

5. Docker (credential-free)

Mount the sibling checkout under /app mirroring the host layout so the override's relative link: path resolves in-container, and install with --no-lockfile:

yaml
admin:
  image: node:22-alpine
  working_dir: /app/<project>/admin
  command: sh -c "corepack enable && (test -e node_modules/@redelay/js-admin || pnpm install --no-frozen-lockfile --no-lockfile) && pnpm dev --host 0.0.0.0 --port 3002"
  volumes:
    - ../../admin:/app/<project>/admin
    - ../../../redelay/js-admin-nuxt4:/app/redelay/js-admin-nuxt4   # override link target

No GitHub token in the container, no committed secret. If a token is ever genuinely needed (e.g. a build that installs the pinned tag), put it in a gitignored env file the compose auto-loads — never a committed one.

The three gotchas (they broke us; they'll break you)

  1. Don't use a bare extends specifier. extends: ['@redelay/js-admin'] fails — Nuxt resolves a bare string through package-entry resolution, and this private, entry-point-less package errors with Cannot extend config from @redelay/js-admin. Extend by the resolved node_modules path.
  2. realpathSync is required. The dev entry is a pnpm symlink. Without resolving it, Nuxt registers the layer's ~/@ aliases against the symlink path while Vite loads the layer's files from their real path — so the layer's own ~/config/... imports fail to resolve.
  3. Tailwind @source must target node_modules/@redelay/js-admin, not the sibling tree — otherwise a versioned/CI build (no sibling checkout) has nothing to scan and the admin renders unstyled.

Bumping the base layer

  1. Tag the redelay/js-admin-nuxt4 repo (the package version lives inside the tagged commit, so a rename or change needs a new tag).
  2. In the project admin: pnpm add github:redelay/js-admin-nuxt4#<newtag>.

Local dev keeps using the linked checkout regardless; the bump only affects fresh clones / CI.