Showcase: how we built agency-site as a demo
agency-site is a fork-and-sync of our main site - it runs as a full demo. Here's how we maintain it without pain, and why it isn't just a storybook.
agency-site is a fork-and-sync of our main site - it runs as a full demo. Here's how we maintain it without pain, and why it isn't just a storybook.
I have the same exchange on most sales calls:
Client: "Do you have a live example I can click through? Not a portfolio with screenshots."
Me: "Yes - agency-site. It's a full copy of our main site with a different brand and different content."
apps/showcase-agency-site isn't a storybook or an isolated demo. It's
a real, deployed application running 95% of the same code as the main
site, with a different brand theme, copy, and content. Here's how it's
organized.
The first instinct was to extract a shared package and import it from both apps:
packages/site-template/ ← shared
apps/site/ ← imports
apps/showcase-agency/ ← imports
That was wrong. Reason: if in 3 months I want to change the hero section on the main site but not on agency-site (because agency-site is a demo snapshot frozen for clients on a specific date), the shared package forces me to add prop-driven configuration that nobody but those two apps needs.
The solution: directory-level fork.
# apps/showcase-agency-site/ is a fork of apps/site/
# Synced through a dedicated script:
./scripts/sync-agency-site-backend.shThe script:
apps/site/backend/ → apps/showcase-agency-site/backend/apps/site/admin-frontend/ → apps/showcase-agency-site/admin-frontend/apps/site/app/ - frontend divergence is allowed.messages/ - copy is a fork, not a sync.This keeps backend + admin always identical while the frontend lives its own life.
ESLint + no-restricted-imports:
// apps/showcase-agency-site/eslint.config.mjs
'no-restricted-imports': ['error', {
patterns: [{
group: ['../../../site/**', '@itsolutions/site/**'],
message: 'Cross-app imports forbidden. Use packages/ for shared code.'
}]
}]Plus dependency-cruiser in CI:
pnpm depcheck
# → exit 1 if anyone imported from another appA sneaky import from apps/site/ into apps/showcase-agency-site/
fails the CI build. That hurts - and that's the point. If something
should be shared, it lands in packages/.
Both stacks live on the same VPS:
itsolutionsdz.pl → apps/site → Caddy port 80/443
demo-agency-site.itsolutionsdz.pl → apps/showcase-agency-site → Caddy port 8082/8443
Each stack has its own docker-compose.prod.yml. Each owns its database.
Each has a separate Let's Encrypt cert. This isn't multi-tenant - it's
two distinct applications that just happen to share hardware.
demo-agency-site.itsolutionsdz.pl/admin with
test credentials (published in the showcase catalog). The client sees
what their panel will look like.“The best showcase is a complete application built for a real use case, whose source you publish as a demo - not a storybook with stock content.
”
Clients see what their project will look like. You keep one repo, one architecture, one deploy flow.
And yes - agency-site gets updates every few weeks when the main site gets a rebrand or a new feature. That maintenance is cheaper than running a third application with stock content.