SuperSite

SUPER SITE

Setup, Architecture & Production Blueprint

A consolidated guide to the configurable React experience, persistent administration service, real-time updates, testing, containers, and delivery pipeline.

Architecture baseline • August 2026\ Reconstructed from the requested scope and available conversation preview

How to use this document

Outcome. SUPER SITE is treated as a data-driven application, not a collection of hand-tuned pages. Administrators publish validated configuration; route-aware selectors resolve it; React components render the result; WebSocket events synchronize active sessions.

The earlier thread itself could not be fully retrieved while this document was generated. The cached exchange established the request for a consolidated archive, while the detailed scope supplied here identifies the intended features. Accordingly, this guide preserves the likely evolution and useful reasoning, but does not claim that reconstructed wording is a verbatim transcript.

Contents

  1. Evolution, decisions, and quality attributes
  2. System context and configuration model
  3. React frontend and route resolution
  4. MoviesScenesGrid and SearchResultsPage integration
  5. Visual controls, SVG motion, effects, loaders, and banners
  6. ode.js API and WebSocket updates
  7. Banner management and authorization
  8. Local mock/testing workflow
  9. Docker, environments, and GitLab CI/CD
  10. Folder structure, setup, run, and deploy
  11. Prototype boundary and production hardening

1. Evolution and architectural reasoning

From page tweaks to a configuration platform

The design naturally evolves through four stages:

  1. Static UI prototype. Grid columns, poster placement, player size, gradients, and animations are coded directly into React components. This proves the visual language quickly.
  2. Shared runtime settings. Common values move into a context so the same controls drive multiple components without prop drilling.
  3. Route-specific overrides. A global default remains the safe baseline; route keys override only the fields that differ. This avoids copying a whole configuration per page.
  4. Persistent, governed configuration. A server stores versioned configuration, validates changes, enforces roles, records audit history, and broadcasts publication events.

The key reasoning is separation of concerns: presentation components should consume resolved values; they should not know whether a value came from defaults, a route override, a user preference, a feature flag, or a persistent administration service.

Quality attributes

  
AttributeDesign response
ConsistencyOne schema and one resolver feed all pages and visual controls.
Safe changeDraft → validate → preview → publish; defaults and rollback remain available.
PerformanceCSS variables for cosmetic updates, memoized selectors, bounded WebSocket messages, cached published config.
AccessibilityReduced motion, keyboard focus, semantic controls, non-color loading states, contrast checks.
OperabilityHealth endpoints, structured logs, version metadata, metrics, immutable container images.
SecurityServer-side authorization, schema validation, audit events, protected secrets, restricted CORS.

2. System context

Browser React application ── GET /api/config/published ──► Node.js API ──► PostgreSQL │ ▲ │ config, users, │ │ └── audit log banners, versions ├── resolved route config│ ├── admin editor ── PUT /api/admin/config/draft └── WebSocket ◄──── config.published / banner.changed GitLab CI ── lint / test / build / scan ──► image registry ──► staging ──► production

Configuration precedence

A predictable merge order prevents surprising UI behavior:

compiled defaults

→ published global settings

→ matching route override (exact, then declared pattern)

→ optional user preference (only whitelisted keys)

→ ephemeral preview patch (admin preview mode only)

Arrays should normally replace rather than merge. Objects merge deeply only for schema-declared sections. An override set to null must have a defined meaning—prefer “remove override and inherit” rather than “render null.”

Canonical TypeScript model

// packages/config/src/schema.ts

import { z } from 'zod';

export const uiSettingsSchema = z.object({

grid: z.object({

minColumns: z.number().int().min(1).max(12).default(2),

maxColumns: z.number().int().min(1).max(12).default(6),

gapPx: z.number().int().min(0).max(64).default(16),

cardMinWidthPx: z.number().int().min(120).max(600).default(220),

}).refine(v => v.minColumns <= v.maxColumns, 'minColumns must be <= maxColumns'),

poster: z.object({

visible: z.boolean().default(true),

position: z.enum( ).default('before-player'),

aspectRatio: z.enum().default('2/3'),

}),

player: z.object({

width: z.string().regex(/^\d+(px|%|vw)$/).default('100%'),

maxWidth: z.string().regex(/^\d+(px|%|vw)$/).default('1280px'),

aspectRatio: z.enum().default('16/9'),

}),

motion: z.object({

enabled: z.boolean().default(true),

preset: z.enum().default('subtle'),

hoverScale: z.number().min(1).max(1.12).default(1.025),

durationMs: z.number().int().min(80).max(1200).default(220),

}),

effects: z.object({

scrollReveal: z.boolean().default(true),

filterTransition: z.boolean().default(true),

gradientBanner: z.boolean().default(true),

}),

});

export type UiSettings = z.infer<typeof uiSettingsSchema>;

export type UiSettingsPatch = PartialDeep<UiSettings>;

export const siteConfigSchema = z.object({

version: z.number().int().nonnegative(),

updatedAt: z.string().datetime(),

defaults: uiSettingsSchema,

routes: z.record(z.string(), uiSettingsSchema.deepPartial()),

banners: z.array(z.object({

id: z.string().uuid(), active: z.boolean(), priority: z.number().int(),

message: z.string().min(1).max(240), href: z.string().url().optional(),

routes: z.array(z.string()).default(),

startsAt: z.string().datetime().nullable(), endsAt: z.string().datetime().nullable(),

gradient: z.tuple(),

})),

});

3. React frontend architecture

Provider stack and responsibilities

<ErrorBoundary>

<QueryClientProvider>

<AuthProvider>

<AdminConfigProvider>

<RouterProvider router={router} />

</AdminConfigProvider>

</AuthProvider>

</QueryClientProvider>

</ErrorBoundary>

Server state belongs in a query/cache layer; authenticated identity belongs in auth state; the configuration provider owns resolution, preview patches, and WebSocket reconciliation. Page components receive resolved settings via a hook and remain ignorant of transport details.

Defaults and route resolver

// apps/web/src/config/defaults.ts

export const DEFAULT_UI: UiSettings = {

grid: { minColumns: 2, maxColumns: 6, gapPx: 16, cardMinWidthPx: 220 },

poster: { visible: true, position: 'before-player', aspectRatio: '2/3' },

player: { width: '100%', maxWidth: '1280px', aspectRatio: '16/9' },

motion: { enabled: true, preset: 'subtle', hoverScale: 1.025, durationMs: 220 },

effects: { scrollReveal: true, filterTransition: true, gradientBanner: true },

};

const ROUTE_ORDER = ;

export function resolveUiSettings(

pathname: string, config: SiteConfig, preview?: UiSettingsPatch

): UiSettings {

const key = ROUTE_ORDER.find(pattern => matchPath({ path: pattern, end: true }, pathname)) ?? '*';

return uiSettingsSchema.parse(deepMerge(

DEFAULT_UI,

config.defaults,

config.routes ?? {},

preview ?? {},

));

}

// Merge only plain objects. Arrays and primitives replace existing values.

export function deepMerge<T>(…sources: unknown[]): T {

return sources.reduce((target: any, source: any) => {

if (!source || typeof source !== 'object' || Array.isArray(source)) return source ?? target;

const out = { …(target ?? {}) };

for (const of Object.entries(source)) {

out = value && typeof value === 'object' && !Array.isArray(value)

? deepMerge(out, value) : value;

}

return out;

}, {}) as T;

}

Route keys are contracts

Do not store arbitrary URLs as keys if the router uses parameterized routes. Store stable patterns such as /movies/:movieId/scenes. Declare matching order because overlapping patterns are otherwise ambiguous. In production, share route identifiers between the router and admin schema so renaming a path cannot silently orphan settings.

4. AdminConfigContext

// apps/web/src/config/AdminConfigContext.tsx

type ConfigContextValue = {

published: SiteConfig;

resolved: UiSettings;

previewPatch: UiSettingsPatch | null;

status: 'loading' | 'ready' | 'stale' | 'error';

setPreviewPatch(patch: UiSettingsPatch | null): void;

refresh(): Promise<void>;

};

const ConfigContext = createContext<ConfigContextValue | null>(null);

export function AdminConfigProvider({ children }: PropsWithChildren) {

const { pathname } = useLocation();

const = useState<SiteConfig | null>(null);

const = useState<UiSettingsPatch | null>(null);

const = useState<ConfigContextValue>('loading');

const refresh = useCallback(async () => {

const response = await fetch('/api/config/published', { credentials: 'include' });

if (!response.ok) throw new Error(`Config fetch failed: ${response.status}`);

const next = siteConfigSchema.parse(await response.json());

setPublished(current => !current || next.version >= current.version ? next : current);

setStatus('ready');

}, []);

useEffect(() => { refresh().catch(() => setStatus('error')); }, );

useEffect(() => connectConfigSocket({

onPublished: event => setPublished(current =>

!current || event.config.version > current.version ? event.config : current),

onDisconnect: () => setStatus('stale'),

}), []);

const effective = published ?? { version: 0, updatedAt: new Date(0).toISOString(),

defaults: DEFAULT_UI, routes: {}, banners: [] };

const resolved = useMemo(

() => resolveUiSettings(pathname, effective, previewPatch ?? undefined),

);

return <ConfigContext.Provider value={{ published: effective, resolved,

previewPatch, status, setPreviewPatch, refresh }}>{children}</ConfigContext.Provider>;

}

export function useUiSettings() {

const value = useContext(ConfigContext);

if (!value) throw new Error('useUiSettings must be inside AdminConfigProvider');

return value;

}

The context keeps the last valid configuration when connectivity drops. A stale badge may appear in the admin UI, but the public site should continue rendering. Incoming versions lower than the current version are ignored to prevent WebSocket reordering from rolling the UI backward.

5. MoviesScenesGrid and SearchResultsPage

One grid primitive, two integrations

// apps/web/src/components/MediaGrid.tsx

export function MediaGrid({ children, ariaLabel }: PropsWithChildren<{ ariaLabel: string }>) {

const { resolved } = useUiSettings();

const style = {

'–grid-min': resolved.grid.minColumns,

'–grid-max': resolved.grid.maxColumns,

'–grid-gap': `${resolved.grid.gapPx}px`,

'–card-min': `${resolved.grid.cardMinWidthPx}px`,

} as CSSProperties;

return <section className="media-grid" style={style} aria-label={ariaLabel}>{children}</section>;

}

// apps/web/src/pages/MoviesScenesGrid.tsx

export function MoviesScenesGrid({ scenes }: { scenes: Scene[] }) {

return <MediaGrid ariaLabel="Movie scenes">

{scenes.map(scene => <SceneCard key={scene.id} scene={scene} />)}

</MediaGrid>;

}

// apps/web/src/pages/SearchResultsPage.tsx

export function SearchResultsPage() {

const { data, isLoading } = useSearchResults();

if (isLoading) return <ResultsSkeleton />;

return <main><SearchToolbar /><MediaGrid ariaLabel="Search results">

{data.items.map(item => <MediaCard key={`${item.kind}:${item.id}`} item={item} />)}

</MediaGrid></main>;

}

/* apps/web/src/styles/media-grid.css */

.media-grid {

display:grid; gap:var(–grid-gap);

grid-template-columns:repeat(auto-fit, minmax(min(100%, var(–card-min)), 1fr));

}

@media (min-width: 720px) {

.media-grid { grid-template-columns:repeat(var(–grid-min), minmax(0,1fr)); }

}

@media (min-width: 1280px) {

.media-grid { grid-template-columns:repeat(var(–grid-max), minmax(0,1fr)); }

}

The same primitive prevents divergence between scenes and search. If the two pages need different columns, configure route overrides; do not fork the component. Clamp card width and column counts in the schema to stop an administrator from producing unusable layouts.

PosterContainer visibility and position

export function MediaDetail({ item }: { item: MediaItem }) {

const { resolved: { poster, player } } = useUiSettings();

const posterNode = poster.visible ? <PosterContainer item={item} mode={poster.position} /> : null;

return <div className={`media-detail poster-${poster.position}`}>

{poster.position === 'before-player' && posterNode}

<PlayerContainer source={item.source} settings={player} />

{poster.position === 'after-player' && posterNode}

{poster.position === 'overlay' && posterNode}

</div>;

}

Conditional rendering is preferred over hiding with CSS: hidden posters should not remain in the accessibility tree or download large images. Overlay mode needs a containing block, an explicit stacking order, readable focus outlines, and a mobile fallback when the overlay would obscure player controls.

PlayerContainer sizing

export function PlayerContainer({ source, settings }: PlayerProps) {

return <div className="player-shell" style={{

width: settings.width,

maxWidth: settings.maxWidth,

aspectRatio: settings.aspectRatio,

}}><video controls preload="metadata" src={source} /></div>;

}

.player-shell { margin-inline:auto; position:relative; background:#05070a; }

.player-shell > video { width:100%; height:100%; object-fit:contain; display:block; }

@media (max-width:600px) { .player-shell { width:100% !important; max-width:100% !important; } }

Allow only schema-validated CSS lengths rather than arbitrary styles. A production system can make sizing even safer by storing typed numbers plus units instead of raw strings.

6. Motion, effects, loaders, and banners

Reusable SVG animation

export function OrbitMark({ title = 'Loading' }: { title?: string }) {

const id = useId();

return <svg viewBox="0 0 64 64" role="img" aria-labelledby={id} className="orbit-mark">

<title id={id}>{title}</title>

<circle cx="32" cy="32" r="22" className="orbit-track" />

<path d="M32 10a22 22 0 0 1 22 22" className="orbit-head" />

</svg>;

}

.orbit-track,.orbit-head { fill:none; stroke-width:4; }

.orbit-track { stroke:currentColor; opacity:.18; }

.orbit-head { stroke:currentColor; stroke-linecap:round; transform-origin:32px 32px;

animation:orbit 900ms linear infinite; }

@keyframes orbit { to { transform:rotate(360deg); } }

@media (prefers-reduced-motion:reduce) { .orbit-head { animation:none; opacity:.75; } }

SVG components should inherit currentColor, expose an accessible label when meaningful, use stable view boxes, and avoid hard-coded instance IDs. Decorative SVGs instead use aria-hidden="true".

Scroll reveal, hover, and filter transitions

export function useReveal(enabled: boolean) {

const ref = useRef<HTMLElement | null>(null);

useEffect(() => {

if (!enabled || !ref.current) return;

const node = ref.current;

const observer = new IntersectionObserver(() => {

if (entry.isIntersecting) { node.dataset.visible = 'true'; observer.disconnect(); }

}, { rootMargin: '0px 0px -8% 0px', threshold: .08 });

observer.observe(node); return () => observer.disconnect();

}, );

return ref;

}

.reveal { opacity:0; transform:translateY(10px); transition:opacity 260ms ease,transform 260ms ease; }

.reveal { opacity:1; transform:none; }

.media-card { transition:transform var(–motion-duration) cubic-bezier(.2,.8,.2,1), box-shadow 180ms ease; }

.media-card:hover { transform:translateY(-2px) scale(var(–hover-scale)); }

.media-card:focus-within { outline:2px solid var(–focus); outline-offset:3px; }

@media (prefers-reduced-motion:reduce) { .reveal,.media-card { transition:none; transform:none; } }

Filter effects should animate opacity or transform, not height-heavy layouts. Keep the previous result set visible while the next request is in flight; announce the new result count through an aria-live="polite" region. Never make hover the sole carrier of information.

Loading and progress states

  • Initial page load: use skeletons that approximate final geometry to reduce layout shift.
  • Inline action: keep the button label, add a compact spinner, disable duplicate submission.
  • Known-duration upload/import: expose a determinate progress bar with numeric value.
  • Unknown-duration work: use an indeterminate indicator and plain-language status.

export function Progress({ value, label }: { value?: number; label: string }) {

const determinate = Number.isFinite(value);

return <div className="progress-wrap"><span>{label}</span>

<div className="progress" role="progressbar" aria-label={label}

aria-valuemin={determinate ? 0 : undefined} aria-valuemax={determinate ? 100 : undefined}

aria-valuenow={determinate ? Math.round(value!) : undefined}>

<i className={determinate ? '' : 'indeterminate'} style={determinate ? { width:`${value}%` } : {}} />

</div></div>;

}

Gradient banner

export function SiteBanner() {

const { published } = useUiSettings();

const banner = selectActiveBanner(published.banners, location.pathname, new Date());

if (!banner) return null;

return <aside className="site-banner" style={{

'–banner-from': banner.gradient, '–banner-to': banner.gradient

} as CSSProperties}>

{banner.href ? <a href={banner.href}>{banner.message}</a> : banner.message}

</aside>;

}

.site-banner { color:#fff; padding:10px 16px; text-align:center;

background:linear-gradient(105deg,var(–banner-from),var(–banner-to)); }

.site-banner a { color:inherit; font-weight:700; text-underline-offset:3px; }

7. Administration UI

Editing workflow

  1. Select global defaults or a named route.
  2. Edit typed controls: bounded number inputs, toggles, segmented position controls, and validated color fields.
  3. Preview in an isolated frame or preview session without modifying the public published version.
  4. Validate locally and on the server.
  5. Save a draft with optimistic concurrency.
  6. Publish after permission and confirmation checks.
  7. Observe the new version propagate; retain rollback to a prior immutable version.

async function saveDraft(baseVersion: number, patch: SiteConfigPatch) {

const response = await fetch('/api/admin/config/draft', {

method: 'PUT', credentials: 'include',

headers: { 'content-type':'application/json', 'if-match': String(baseVersion) },

body: JSON.stringify(patch),

});

if (response.status === 409) throw new ConfigConflictError(await response.json());

if (!response.ok) throw new Error('Draft save failed');

return siteConfigSchema.parse(await response.json());

}

If-Match (or an equivalent base-version field) prevents two administrators from silently overwriting each other. A conflict screen should show which version changed and offer reload or an explicit merge.

8. Persistent Node.js API

Suggested endpoints

    
MethodPathAccessPurpose
GET/health/livePublic/internalProcess liveness
GET/health/readyInternalDatabase and dependency readiness
GET/api/config/publishedPublicCurrent public configuration + version/ETag
GET/api/admin/config/draftEditor+Current draft
PUT/api/admin/config/draftEditor+Validate and save draft
POST/api/admin/config/publishPublisher+Create immutable published version
POST/api/admin/config/rollback/:versionAdminRepublish a historical version as a new version
CRUD/api/admin/bannersEditor+Manage scheduled, route-targeted banners

Fastify service example

// apps/api/src/server.ts

const app = Fastify({ logger: true, trustProxy: true });

await app.register(cookie);

await app.register(cors, { origin: env.WEB_ORIGINS, credentials: true });

await app.register(rateLimit, { max: 200, timeWindow: '1 minute' });

app.get('/api/config/published', async (req, reply) => {

const config = await configRepo.getPublished();

const etag = `W/"config-${config.version}"`;

if (req.headers === etag) return reply.code(304).send();

return reply.header('etag', etag).header('cache-control','public,max-age=30,stale-while-revalidate=300').send(config);

});

app.put('/api/admin/config/draft', {

preHandler:

}, async (req, reply) => {

const baseVersion = Number(req.headers );

const patch = siteConfigSchema.deepPartial().parse(req.body);

const draft = await db.transaction(async tx => {

const current = await configRepo.getDraft(tx, { forUpdate: true });

if (current.version !== baseVersion) throw new ConflictError(current.version);

const next = siteConfigSchema.parse(deepMerge(current.document, patch));

const saved = await configRepo.saveDraft(tx, next, req.user.id);

await auditRepo.append(tx, { actorId:req.user.id, action:'config.draft.saved',

entityId:String(saved.version), before:current.document, after:saved.document });

return saved;

});

return reply.send(draft.document);

});

app.post('/api/admin/config/publish', {

preHandler:

}, async (req, reply) => {

const published = await publishService.publish(req.user.id);

socketHub.broadcast({ type:'config.published', version:published.version, config:published });

return reply.code(201).send(published);

});

Persistence model

CREATE TABLE config_versions (

version BIGSERIAL PRIMARY KEY,

status TEXT NOT NULL CHECK (status IN ('draft','published','archived')),

document JSONB NOT NULL,

created_by UUID NOT NULL REFERENCES users(id),

created_at TIMESTAMPTZ NOT NULL DEFAULT now(),

published_at TIMESTAMPTZ

);

CREATE UNIQUE INDEX one_draft ON config_versions(status) WHERE status='draft';

CREATE INDEX published_versions ON config_versions(published_at DESC) WHERE status='published';

CREATE TABLE audit_events (

id UUID PRIMARY KEY, actor_id UUID REFERENCES users(id), action TEXT NOT NULL,

entity_type TEXT NOT NULL, entity_id TEXT NOT NULL,

before_data JSONB, after_data JSONB, request_id TEXT,

created_at TIMESTAMPTZ NOT NULL DEFAULT now()

);

Configuration JSONB keeps schema-shaped documents easy to version, while relational tables remain preferable for users, roles, sessions, and banners when they require rich querying. Validate both at the API boundary and before persistence.

9. WebSocket real-time updates

// Server message envelope

type ServerEvent =

| { type:'hello'; latestVersion:number; heartbeatMs:number }

| { type:'config.published'; version:number; config:SiteConfig }

| { type:'banner.changed'; version:number }

| { type:'error'; code:string; message:string };

// Browser connector with backoff and version recovery

export function connectConfigSocket(handlers: Handlers) {

let socket: WebSocket | undefined; let attempt = 0; let stopped = false;

const open = () => {

socket = new WebSocket(`${location.protocol === 'https:' ? 'wss' : 'ws'}://${location.host}/ws/config`);

socket.onopen = () => { attempt = 0; };

socket.onmessage = event => {

const message = serverEventSchema.parse(JSON.parse(event.data));

if (message.type === 'config.published') handlers.onPublished(message);

if (message.type === 'hello') handlers.onHello?.(message);

};

socket.onclose = () => {

handlers.onDisconnect?.();

if (!stopped) setTimeout(open, Math.min(30_000, 500 * 2 ** attempt++) + Math.random()*400);

};

};

open(); return () => { stopped = true; socket?.close(1000, 'unmount'); };

}

Broadcast publication events, not every keystroke. In a multi-instance API deployment, use PostgreSQL LISTEN/NOTIFY, Redis pub/sub, or another broker so clients connected to different pods receive the same event. On reconnect, compare versions and fetch the published REST resource if a gap exists. Apply message-size limits, origin checks, authentication for admin channels, heartbeat/idle timeouts, and per-connection rate limits.

10. Banner management and roles

  1. Active flag is true.
  2. Current time falls between optional start/end boundaries, evaluated in UTC.
  3. Route pattern matches.
  4. Highest priority wins; ties use newest update, then stable ID.

Validate message length, URLs, route patterns, gradient colors, and schedule order. For urgent operational banners, support an expiration time by default so old incidents do not stay visible indefinitely.

Role matrix

     
CapabilityViewerEditorPublisherAdmin
View published settingsYesYesYesYes
View draft/historyOptionalYesYesYes
Edit draft/bannerNoYesYesYes
PublishNoNoYesYes
Rollback, manage rolesNoNoNoYes

Permissions must be enforced server-side on every operation. Hiding a button is only presentation. Prefer identity-provider groups mapped to application roles; use short-lived sessions or tokens and CSRF protection when cookie authentication is used.

11. Local mock and testing setup

Mock transport

// apps/web/src/mocks/handlers.ts (MSW)

export const handlers = [

http.get('/api/config/published', () => HttpResponse.json(seedConfig)),

http.put('/api/admin/config/draft', async ({ request }) => {

const patch = await request.json();

return HttpResponse.json(siteConfigSchema.parse(deepMerge(seedConfig, patch)));

}),

];

// Enable only in development

if (import.meta.env.DEV && import.meta.env.VITE_USE_MOCKS === 'true') {

const { worker } = await import('./mocks/browser'); await worker.start();

}

Test pyramid

  • Unit: schema boundaries, deep merge semantics, route precedence, banner scheduling, permission guards.
  • Component: column CSS variables, hidden poster removal, player sizing, reduced-motion behavior, admin validation errors.
  • API integration: draft conflict, transaction rollback, publish event, authorization, ETag/304.
  • End to end: editor saves draft; publisher publishes; a second browser receives the WebSocket update; rollback restores a prior appearance.
  • Visual/accessibility: representative viewports, keyboard navigation, axe checks, contrast, no animation under reduced motion.

it('applies exact route override after defaults', () => {

const resolved = resolveUiSettings('/search', {

…seedConfig,

defaults: { …DEFAULT_UI, grid:{ …DEFAULT_UI.grid, maxColumns:5 } },

routes: { '/search': { grid:{ maxColumns:8 } } },

});

expect(resolved.grid.maxColumns).toBe(8);

expect(resolved.grid.gapPx).toBe(DEFAULT_UI.grid.gapPx);

});

12. Recommended folder structure

super-site/

├─ apps/

│ ├─ web/

│ │ ├─ src/{app,components,config,features,pages,mocks,styles,test}/

│ │ ├─ public/ Dockerfile vite.config.ts

│ └─ api/

│ ├─ src/{auth,config,banners,db,http,ws,observability,test}/

│ ├─ migrations/ Dockerfile

├─ packages/

│ ├─ config/ # shared Zod schema, types, defaults, route IDs

│ ├─ ui/ # reusable primitives and SVG components

│ ├─ eslint-config/ # organization lint rules

│ └─ test-utils/

├─ deploy/

│ ├─ compose/ kubernetes/ nginx/

├─ .gitlab-ci.yml compose.yaml package.json pnpm-workspace.yaml

└─ .env.example README.md

A monorepo makes the schema and event contracts shareable without publishing them externally. Keep feature logic close to the feature; reserve the UI package for genuinely reused, application-agnostic primitives.

13. Environment configuration

   
VariableExampleNotes
NODE_ENVproductionRuntime mode, never a secret
PORT8080API listen port
DATABASE_URLpostgres://…Secret; inject from protected store
WEB_ORIGINShttps://staging.example.comExplicit comma-separated origins
SESSION_SECRETLong random secret; rotate deliberately
LOG_LEVELinfoUse debug only temporarily
VITE_API_BASE_URL/apiBuild-time public value
VITE_WS_URLwss://…/ws/configOptional if same-origin
VITE_USE_MOCKSfalseDevelopment only; fail production build if true

Commit .env.example, not real secrets. Validate environment variables at startup and exit with a clear error when required values are missing.

14. Docker: local, staging, and production

# apps/api/Dockerfile

FROM node:22-alpine AS deps

WORKDIR /app

COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./

COPY apps/api/package.json apps/api/package.json

COPY packages/config/package.json packages/config/package.json

RUN corepack enable && pnpm install –frozen-lockfile

FROM deps AS build

COPY . .

RUN pnpm –filter @supersite/api… build && pnpm deploy –filter @supersite/api –prod /out

FROM node:22-alpine AS runtime

ENV NODE_ENV=production

RUN addgroup -S app && adduser -S app -G app

WORKDIR /app

COPY –from=build –chown=app:app /out ./

USER app

EXPOSE 8080

HEALTHCHECK –interval=30s –timeout=3s CMD wget -qO- http://127.0.0.1:8080/health/live || exit 1

CMD

# compose.yaml (local development shape)

services:

db:

image: postgres:17-alpine

environment:

POSTGRES_DB: supersite

POSTGRES_USER: supersite

POSTGRES_PASSWORD: local-only

volumes:

healthcheck: { test: , interval: 5s, timeout: 3s, retries: 20 }

api:

build: { context: ., dockerfile: apps/api/Dockerfile, target: build }

environment:

NODE_ENV: development

DATABASE_URL: postgres://supersite:local-only@db:5432/supersite

WEB_ORIGINS: http://localhost:5173

depends_on: { db: { condition: service_healthy } }

ports:

web:

build: { context: ., dockerfile: apps/web/Dockerfile }

environment: { VITE_API_BASE_URL: http://localhost:8080/api }

ports:

volumes: { pgdata: {} }

Use the same image in staging and production, promoted by digest. Only configuration and secrets differ. Run database migrations as a controlled release job, not simultaneously in every API replica. Terminate TLS at the ingress and preserve WebSocket upgrade headers.

15. GitLab CI/CD

stages:

default:

image: node:22-alpine

cache: { key: "$CI_COMMIT_REF_SLUG", paths: }

before_script:

– corepack enable

– pnpm config set store-dir .pnpm-store

– pnpm install –frozen-lockfile

lint:

stage: validate

script:

unit_test:

stage: test

script:

coverage: '/All files\*\\*\s+( +)/'

artifacts: { when: always, reports: { junit: reports/junit.xml } }

build_apps:

stage: build

script:

artifacts: { paths: , expire_in: 1 week }

container_images:

stage: build

image: docker:27

services:

before_script:

script:

– docker build -f apps/api/Dockerfile -t $CI_REGISTRY_IMAGE/api:$CI_COMMIT_SHA .

– docker build -f apps/web/Dockerfile -t $CI_REGISTRY_IMAGE/web:$CI_COMMIT_SHA .

– docker push $CI_REGISTRY_IMAGE/api:$CI_COMMIT_SHA

– docker push $CI_REGISTRY_IMAGE/web:$CI_COMMIT_SHA

deploy_staging:

stage: deploy

environment: { name: staging, url: https://staging.example.com }

script:

rules:

deploy_production:

stage: deploy

environment: { name: production, url: https://example.com }

script:

rules:

when: manual

allow_failure: false

Add dependency/container scanning, secret detection, an end-to-end staging smoke test, and protected production environments. A strong pipeline promotes the previously tested digest; it does not rebuild source during production deployment.

16. Setup, run, and deploy

Prerequisites

  • Node.js 22 and Corepack/pnpm
  • Docker with Compose
  • PostgreSQL 17 when running without containers
  • A GitLab project with registry and protected environment variables

Local development

  1. Copy .env.example to .env.local and use development-only values.
  2. Run corepack enable and pnpm install –frozen-lockfile.
  3. Start PostgreSQL with docker compose up -d db.
  4. Apply migrations: pnpm –filter @supersite/api db:migrate.
  5. Seed a local admin and configuration: pnpm –filter @supersite/api db:seed.
  6. Start both apps: pnpm dev.
  7. Open the web URL, publish a test configuration, and confirm a second tab updates immediately.

Test and build

pnpm lint

pnpm typecheck

pnpm test

pnpm test:e2e

pnpm build

docker compose build

docker compose up –wait

Staging release

  1. Merge through a reviewed merge request.
  2. CI validates, tests, builds, scans, and pushes images tagged by commit SHA.
  3. The default branch deploys the digest to staging.
  4. Run migrations once, then readiness and smoke tests.
  5. Exercise config edit/publish, banners, WebSocket propagation, and rollback.

Production release

  1. Create a semantic version tag from the tested commit.
  2. Approve the protected manual deployment.
  3. Run backward-compatible migrations.
  4. Roll out API and web images by immutable digest.
  5. Watch error rate, latency, readiness, socket connections, and configuration publication metrics.
  6. Rollback the application digest if unhealthy; rollback configuration by publishing a prior version as a new version.

17. Prototype/example code vs. production hardening

Important. The snippets in this guide are coherent reference implementations, not a copy-paste production system. They demonstrate contracts and control flow; production work must close the items below.

  
Example/prototypeProduction hardening
In-memory socket hubCross-instance broker, delivery metrics, heartbeat, connection limits, graceful shutdown.
Basic role middlewareIdentity-provider integration, fine-grained permissions, session revocation, CSRF, secure cookies.
JSONB configurationSchema migrations, immutable history, transactional publication, backups, retention, disaster recovery.
Simple deep mergeProperty allowlist, prototype-pollution defense, explicit array/null semantics, schema versioning.
Raw gradient/color inputsStrict parsing, contrast validation, branded palette option, safe-link policy.
Basic CI exampleLocked images, SBOM/signing, SAST, dependency and container scans, provenance, deploy freeze controls.
Single-region deploymentCapacity model, autoscaling, connection draining, broker resiliency, regional failure strategy.
Console-level observabilityStructured logs with request IDs, RED metrics, traces, dashboards, alerts, SLOs and runbooks.
Functional accessibilityAutomated axe plus manual screen-reader, keyboard, zoom, contrast, and reduced-motion testing.
Basic admin previewIsolated preview origin/frame, CSP, explicit draft labels, conflict UX, approval workflow where needed.

Production acceptance checklist

  • Every accepted configuration validates against an explicit versioned schema.
  • Publication and audit write are atomic; rollback creates a new auditable version.
  • Route identifiers are shared and tested; overrides cannot orphan silently.
  • Public rendering survives API/WebSocket outages using last-known valid configuration.
  • Admin writes have authentication, authorization, CSRF protection, rate limiting, and audit events.
  • WebSocket reconnect catches version gaps and works through the production proxy.
  • Motion respects reduced-motion settings; all controls are keyboard and screen-reader usable.
  • Images, fonts, and video behavior are measured under realistic network and device constraints.
  • Staging uses the production image and deployment topology.
  • Backups and restore procedures are tested, not merely documented.

18. Recommended first implementation sequence

  1. Land shared schema, compiled defaults, and route resolver with unit tests.
  2. Refactor MoviesScenesGrid and SearchResultsPage onto MediaGrid.
  3. Add PosterContainer and PlayerContainer settings with accessibility-safe rendering.
  4. Add AdminConfigContext using mocked published config; implement preview.
  5. Build persistent API, migrations, optimistic draft saves, publication, audit trail.
  6. Add WebSocket publication and reconnect recovery.
  7. Build banner scheduler and role-restricted admin workflows.
  8. Add component/API/E2E/accessibility tests.
  9. Containerize, then establish GitLab staging promotion and production approval.
  10. Instrument SLOs, practice rollback, and close the hardening checklist.

End of SUPER SITE Setup, Architecture & Production Blueprint.