The repo is two apps, app and admin, built from one shared/ folder. Shared is not a package either app installs. It is source that each app compiles in, which makes it behave differently from a library in one way that matters, and that is most of what this page is about.
Anything the apps have in common lives in shared/. app/src and admin/src hold only what is specific to that app, its views, its routes, its layouts and the models for the data it shows.
Shared code imports a few modules through the @ alias, which resolves to whichever app is being built. Those are config/settings, config/dialogs, models/settings, models/auth and, through the @router alias, the router's routes and middleware. So the one copy of a shared page or plugin picks up that app's settings, its dialog registry, its user shape and its routes without shared knowing which app it is in.
The cost is that shared only builds inside an app that provides those modules at those paths. They are a contract, and the first thing a new app has to satisfy.
Settings is the clearest case. Shared declares only the fields shared code reads, and each app widens the same interface with a declare module in its own models/settings.ts, so Settings has more fields inside an app than it does in shared. The values stack up the way Settings describes.
Each app has its own entry, env, vite config, tsconfig, router, layouts, translations and favicons. Admin points VITE_API_URL at the API's /admin prefix, so the same http plugin and the same style of composable talk to a different set of endpoints with nothing else changing.
Start from a copy of an existing app folder, since the vite config, tsconfig and aliases are the same across them. Add it to workspaces in the root package.json with its own dev, build and preview scripts, give it its own port in its env, and add a service in docker-compose.yml with APP set to its name and a case for it in ./dev. Then make sure it provides the modules above, its own routes and middleware included, and shared works inside it.