apps/admin isolation
apps/admin is the internal provisioner — a separate Astro app that creates and configures client sites. It deploys to its own Netlify site with its own astro build. That build does not run build:packages.
The problem
Section titled “The problem”Because apps/admin’s Netlify build runs without building the workspace packages, there is no dist/ directory inside any @drawnagency/* package at deploy time. A runtime (value) import like:
import { slugifyAudienceName } from "@drawnagency/primitives";would fail with a module-not-found error at build time on Netlify — because Node can’t find @drawnagency/primitives/dist/index.js.
The rule
Section titled “The rule”apps/admin/src may import @drawnagency/* only as import type:
// Correct — type import is erased by esbuild, no dist neededimport type { Audience } from "@drawnagency/primitives";
// Wrong — runtime import fails when dist/ is absentimport { Audience } from "@drawnagency/primitives";
// Also wrong — inline type-only still flagged; hoist it to `import type`import { type Audience } from "@drawnagency/primitives";Type imports are erased by esbuild during astro build and have no runtime presence. import { type X } (inline type-only) is also erased but is flagged by the static checker anyway — hoist it to a top-level import type statement.
Enforcement
Section titled “Enforcement”Two gates in CI catch violations:
Layer 1: static check
Section titled “Layer 1: static check”node scripts/check-admin-isolation.mjsscripts/check-admin-isolation.mjs walks every .ts, .tsx, .astro, .mts, and .cts file under apps/admin/src/. It strips comments (preserving string literals so import specifiers remain visible) then pattern-matches for runtime @drawnagency/* imports: import/export ... from "@drawnagency/...", bare side-effect imports, dynamic import(...), and require(...). Any match is a violation.
This runs in CI before build:packages, so it does not depend on the packages being built.
Layer 2: build with dist absent
Section titled “Layer 2: build with dist absent”pnpm --filter portal-admin buildAlso runs before build:packages in CI. With no dist/ present, any runtime import causes an actual build failure — the exact Netlify failure mode. This is the practical proof that the static gate is working.
Duplicating values locally
Section titled “Duplicating values locally”When apps/admin needs a value that exists in @drawnagency/primitives, duplicate it locally rather than importing it. Example: apps/admin/src/lib/slugify.ts provides slugifySubdomain and slugifyAudienceName — functions that are byte-for-byte equivalent to the primitives originals but live entirely in admin:
// Audience slug — kept byte-for-byte equivalent to @drawnagency/primitives'// slugifyAudienceName (schemas/audience.ts). Duplicated here on purpose:// apps/admin must stay free of primitives runtime imports.export function slugifyAudienceName(input: string): string { return input .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, "");}If the canonical function in primitives changes, the admin copy must be updated to match. The comment on the admin copy documents this intentional duplication.