Shared Libraries And Scaffolder · How it works

8 min read
Mid-level10 min read
Rapid overview

How it works

The reuse philosophy: "extract on the second use"

The single most important rule in this codebase is:

Extract a shared library on the SECOND use, not the first.

The first time you write something — a way to filter rows by tenant, a way to send a templated email — you write it inline, inside the product that needs it. You do not immediately generalize it into a package. Why not? Because you only have one concrete example, and one example is not enough to know which parts are genuinely shared versus accidental to that product. Extracting too early produces a premature abstraction: a "shared" API shaped by a single caller, full of options nobody else needs and missing the seams the second caller will actually want.

When a second product needs the same capability, that's the trigger. Now you have two real call sites, you can see what truly varies, and you can lift the common core into a package with a deliberate, injectable shape. This is the classic "rule of three" relaxed to a rule of two for cross-cutting plumbing, where the cost of duplication is high and the concern is well understood.

A few corollaries that follow from this philosophy:

once up front. The shared libraries are the result of repeatedly hitting "second use", not a speculative framework written before any product existed.

out to extract something and discovered only one real consumer (n = 1), the honest move was to defer the package and keep the code inline. "We thought we had two uses; we had one" is a legitimate reason to not build a library.

divergent needs, the divergence was accepted rather than forced through a shared abstraction.

  • The platform was refactored into tiers over time, not designed all at
  • Audits sometimes shrink the scope of a planned package. When a team set
  • Shared does not mean identical. Where two products had legitimately

Keep this rule in your head as you read the catalogues below: every package listed exists because at least two products needed it.

Shared .NET NuGet packages (backend)

Each backend package solves exactly one cross-cutting concern, so a service can pull in only what it needs. They are the building blocks every backend microservice composes from.

PackageWhat it gives you
Identity.Keycloak.AuthorizationKeycloak-based authentication plus the per-realm "realm wall" — each product lives in its own Keycloak realm, and this package enforces that a token from one realm cannot be used against another product's API.
MultiTenancy packageBaseTenantEntity and automatic per-tenant query filtering — every query is scoped to the current tenant by default, so a developer can't accidentally leak one tenant's rows to another.
Messaging.RabbitMq + Messaging.ContractsThe RabbitMQ / MassTransit message bus wiring (Messaging.RabbitMq) plus the shared event contracts (Messaging.Contracts) that producers and consumers agree on, so services communicate asynchronously without sharing a database.
Subscriptions.AspNetCoreBilling / subscription status and gating — ask "is this tenant subscribed / on what plan?" and gate premium features (e.g. remove a watermark, unlock a feature) accordingly.
Marketing.AspNetCoreBulk / marketing email over SMTP, including a compliant one-click unsubscribe flow.
Storage.S3S3-compatible object storage — upload, fetch, and manage blobs (images, uploads) against any S3 API.
Email.Templating + Email.SmtpA shared localized (en/el) email layout (Email.Templating) plus the SMTP transport (Email.Smtp) to actually send transactional mail.
CustomDomains.AspNetCoreBring-your-own custom domain support — a tenant points their own domain at the platform and the request is routed to their content.
Push.ExpoMobile push notifications delivered via Expo's push service.
logging / metrics / health / rate-limiting packagesSmaller cross-cutting utilities: structured logging, metrics, health checks, and rate limiting, shared so every service observes and protects itself the same way.

The shape of a backend service, then, is mostly composition: reference the packages whose concerns you need, register them at startup, and write only the product-specific domain logic on top.

Shared frontend @dloizides/* NPM packages

The frontends are built on a React Native Web stack (the same component code runs as a web app / PWA). The shared frontend tier is published under the @dloizides/* scope and splits into a UI kit, app-infrastructure packages, and some generation/game helpers.

The shared UI kit:

PackageWhat it gives you
@dloizides/ui-feedbackToasts and user feedback, plus a shared useUi context every app shares.
@dloizides/ui-formsShared form components.
@dloizides/ui-tablesShared table / data-grid components.
@dloizides/ui-iconsShared icon set.
@dloizides/legal-uiLegal / consent UI (e.g. terms, privacy surfaces).
@dloizides/ui-layoutShared layout primitives (shells, containers).
@dloizides/utilsShared utility functions.

App-infrastructure packages:

PackageWhat it gives you
@dloizides/auth-webLogin / SSO flow for the frontend.
@dloizides/bff-web-clientA typed client to the product's BFF (backend-for-frontend).
@dloizides/tenant-theme-webPer-tenant theming — a tenant's colours/branding applied at runtime.
@dloizides/orval-presetA preset that generates API hooks from OpenAPI (via Orval), so the frontend's data layer is generated, not hand-written.
@dloizides/pwa-swThe service worker — offline support / PWA behaviour.
@dloizides/frontend-devtoolsShared frontend developer tooling.
@dloizides/analytics-webA wrapper around Umami web analytics.

Generation helpers and game libraries:

PackageWhat it gives you
@dloizides/og-image-generatorBuilds Open Graph images at build time.
@dloizides/text-generationProvider-agnostic LLM text generation — the same code can drive an in-browser model or a remote API behind an injected provider.
@dloizides/event-landing-kitBuilding blocks for event landing pages.
@dloizides/game-inputGame input helpers (e.g. haptics).
@dloizides/save-stateSave slots / persisted game state.

The throughline mirrors the backend: a frontend app is mostly assembly of these packages, with product screens written on top.

How packages are distributed

The two ecosystems are distributed differently, and the distinction matters for how a new service or app picks them up.

In-house .NET packages are not published to nuget.org. They ship via a private "local-packages" feed:

from the local feed (not the public one), and

container builds can restore them.

  • the built .nupkg files are committed into the repo,
  • a package source mapping entry tells NuGet to resolve those package IDs
  • a Dockerfile COPY brings the local feed into the build image so CI and
<!-- a service points its restore at the committed local feed -->
<packageSourceMapping>
  <packageSource key="local-packages">
    <package pattern="Identity.Keycloak.Authorization" />
    <package pattern="MultiTenancy*" />
    <!-- ...the other in-house package IDs... -->
  </packageSource>
</packageSourceMapping>

*The @dloizides/ NPM packages are published to npm (and mirrored to a private GitHub registry**). A frontend installs them like any other dependency.

One distribution detail is worth calling out because it shapes day-to-day work: adoption goes through thin "shim" modules. When an app adopts a shared package, it doesn't rewrite every import across the codebase to point at the new package. Instead it creates a small local module that simply re-exports from the shared package, and existing code keeps importing from the old local path.

// src/components/Toast.ts  — a shim so existing imports don't churn
export { Toast, useUi } from "@dloizides/ui-feedback";

This keeps the diff for adopting a package small and reversible, and lets a migration happen package-by-package instead of in one giant rewrite. (Once an app fully adopts a package and a local file has no remaining purpose, the shim file should be deleted rather than left as dead code.)

The create-dloizides-app scaffolder

The capstone of the whole reuse effort is create-dloizides-app, invoked as:

manage.sh create-app

It is an end-to-end product scaffolder: one command that produces a new, runnable product wired onto the shared stack. It works by composing four generators into a single new product directory:

Clean-Architecture project layout, a sample aggregate/endpoint/migration, wired onto the shared backend packages).

dashboard, the shared @dloizides/* stack, providers, analytics, theming).

app's typed client talks to.

  1. scaffold-backend — generates a .NET backend service skeleton (the
  2. scaffold-web — generates the React Native Web / PWA app shell (login →
  3. a BFF generator — generates the thin backend-for-frontend that the web
  4. mobile config — generates the mobile (Expo) configuration baseline.

On top of composing those generators, create-app does several things that make the output immediately useful:

references the shared NuGet packages; the new frontend depends on the shared @dloizides/* packages.

seams. You opt into capabilities — billing, storage, custom-domains, email, GDPR** — and the scaffolder drops in working (compiling) skeleton code at the predefined seams for each. You then fill in product behaviour; you don't wire the plumbing.

brandable from day one.

register the new service (its entries in the platform's manifests / Tilt / routing).

generator cannot do for you because they require external accounts or secrets: DNS, the Keycloak realm, Stripe, the S3 bucket, the email mailbox, the analytics site.

  • Wires the product onto the shared package stack — the new backend
  • **Auto-injects optional feature toggles as compiling skeletons at known
  • Wires frontend analytics + theming so the app is observable and
  • Emits a REGISTRATION.md — the snippets the owner pastes elsewhere to
  • Emits a per-product RUNBOOK.md — the owner-gated setup steps that a

Critically, the generated app builds and runs — you can log in and reach a dashboard — before any product features are added. That "it already runs" property is the proof that the scaffolder really is synthesis of shipped packages: if the bricks weren't solid and composable, the starter house wouldn't stand on its own.

And to restate the boundary: the scaffolder is synthesis, not extraction. It assembles packages that already exist. It is not the place to discover or build a new abstraction — that happens earlier, at the "second use" moment, inside a real product.


See also