# Setup Templates & Checklists Quick-reference templates for recurring setup tasks. Copy and customize for new projects. ## Table of Contents 1. [New SvelteKit Web App](#1-new-sveltekit-web-app) 2. [New NestJS Backend](#2-new-nestjs-backend) 3. [Deploying New Service to Staging](#3-deploying-new-service-to-staging) 4. [Adding Backend to ManaCore Dashboard](#4-adding-backend-to-manacore-dashboard) 5. [Quick Reference Port Assignments](#5-quick-reference-port-assignments) --- ## 1. New SvelteKit Web App ### Checklist - [ ] Create `src/hooks.server.ts` with runtime env injection - [ ] Update auth store to use `getAuthUrl()` pattern - [ ] Update user-settings store to use `getAuthUrl()` pattern - [ ] Update any API services to use lazy client initialization - [ ] Add Dockerfile with pnpm symlink preservation - [ ] Add to `docker-compose.staging.yml` with both internal and client URLs - [ ] Test locally with `pnpm dev` - [ ] Deploy and verify `window.__PUBLIC_*__` variables in browser console ### Template: hooks.server.ts ```typescript // src/hooks.server.ts import type { Handle } from '@sveltejs/kit'; // Runtime environment variables for client-side injection const PUBLIC_MANA_CORE_AUTH_URL_CLIENT = process.env.PUBLIC_MANA_CORE_AUTH_URL_CLIENT || process.env.PUBLIC_MANA_CORE_AUTH_URL || ''; const PUBLIC_BACKEND_URL_CLIENT = process.env.PUBLIC_BACKEND_URL_CLIENT || process.env.PUBLIC_BACKEND_URL || ''; export const handle: Handle = async ({ event, resolve }) => { return resolve(event, { transformPageChunk: ({ html }) => { const envScript = ``; return html.replace('', `${envScript}`); }, }); }; ``` ### Template: getAuthUrl() Pattern ```typescript // src/lib/stores/auth.svelte.ts import { browser } from '$app/environment'; function getAuthUrl(): string { if (browser && typeof window !== 'undefined') { const injectedUrl = (window as unknown as { __PUBLIC_MANA_CORE_AUTH_URL__?: string }) .__PUBLIC_MANA_CORE_AUTH_URL__; return injectedUrl || 'http://localhost:3001'; } return process.env.PUBLIC_MANA_CORE_AUTH_URL || 'http://localhost:3001'; } // Usage const auth = initializeWebAuth({ baseUrl: getAuthUrl() }); ``` ### Template: Lazy API Client Initialization ```typescript // src/lib/api/services/myservice.ts import { browser } from '$app/environment'; import { createApiClient } from '../base-client'; function getApiUrl(): string { if (browser && typeof window !== 'undefined') { const injectedUrl = (window as unknown as { __PUBLIC_BACKEND_URL__?: string }) .__PUBLIC_BACKEND_URL__; if (injectedUrl) { return `${injectedUrl}/api/v1`; } } return 'http://localhost:3000/api/v1'; } // IMPORTANT: Lazy initialization - don't create client at module level! let _client: ReturnType | null = null; function getClient() { if (!_client) { _client = createApiClient(getApiUrl()); } return _client; } export async function getData() { return getClient().get('/data'); } ``` ### Template: Dockerfile (SvelteKit + pnpm) ```dockerfile # Build stage FROM node:20-alpine AS builder RUN npm install -g pnpm@9.15.0 WORKDIR /app # Copy workspace files COPY pnpm-lock.yaml pnpm-workspace.yaml package.json ./ COPY apps/MYPROJECT/apps/web/package.json apps/MYPROJECT/apps/web/ COPY packages/ packages/ # Install all dependencies RUN pnpm install --frozen-lockfile # Copy source and build COPY apps/MYPROJECT/apps/web apps/MYPROJECT/apps/web RUN pnpm --filter @myproject/web build # Production stage - PRESERVE PNPM SYMLINK STRUCTURE FROM node:20-alpine AS production # Keep same directory structure as builder WORKDIR /app/apps/MYPROJECT/apps/web # Copy pnpm store (target of symlinks) COPY --from=builder /app/node_modules/.pnpm /app/node_modules/.pnpm # Copy app's node_modules (contains symlinks) COPY --from=builder /app/apps/MYPROJECT/apps/web/node_modules ./node_modules # Copy built app COPY --from=builder /app/apps/MYPROJECT/apps/web/build ./build COPY --from=builder /app/apps/MYPROJECT/apps/web/package.json ./ EXPOSE 5173 CMD ["node", "build"] ``` ### Template: docker-compose.staging.yml Entry ```yaml myproject-web: image: ghcr.io/memo-2023/myproject-web:${MYPROJECT_WEB_VERSION:-latest} container_name: myproject-web-staging restart: unless-stopped ports: - '51XX:5173' environment: NODE_ENV: production # Server-side URLs (Docker internal network) PUBLIC_BACKEND_URL: http://myproject-backend:30XX PUBLIC_MANA_CORE_AUTH_URL: http://mana-core-auth:3001 # Client-side URLs (browser access via public IP) PUBLIC_BACKEND_URL_CLIENT: http://46.224.108.214:30XX PUBLIC_MANA_CORE_AUTH_URL_CLIENT: http://46.224.108.214:3001 depends_on: myproject-backend: condition: service_healthy healthcheck: test: ['CMD', 'wget', '--no-verbose', '--tries=1', '--spider', 'http://localhost:5173/health'] interval: 30s timeout: 10s retries: 3 start_period: 40s networks: - manacore-network ``` --- ## 2. New NestJS Backend ### Checklist - [ ] Use `text` type for all `user_id` columns (NOT `uuid`) - [ ] Add health check endpoint at `/api/v1/health` - [ ] Configure CORS to include manacore-web origin (port 5173) - [ ] Add database to `docker/init-db/01-create-databases.sql` - [ ] Add to `scripts/setup-databases.sh` - [ ] Add `dev:myproject:full` command to root `package.json` - [ ] Add Dockerfile with correct health check - [ ] Add to `docker-compose.staging.yml` with proper CORS config ### Template: Drizzle Schema (user_id as text) ```typescript // src/db/schema/main.schema.ts import { pgTable, uuid, text, timestamp } from 'drizzle-orm/pg-core'; export const items = pgTable('items', { id: uuid('id').defaultRandom().primaryKey(), userId: text('user_id').notNull(), // ALWAYS text, not uuid! title: text('title').notNull(), createdAt: timestamp('created_at').defaultNow().notNull(), updatedAt: timestamp('updated_at').defaultNow().notNull(), }); ``` ### Template: Health Controller ```typescript // src/health/health.controller.ts import { Controller, Get } from '@nestjs/common'; @Controller('api/v1/health') export class HealthController { @Get() check() { return { status: 'ok', timestamp: new Date().toISOString() }; } } ``` ### Template: CORS Configuration ```typescript // src/main.ts async function bootstrap() { const app = await NestFactory.create(AppModule); const corsOrigins = process.env.CORS_ORIGINS?.split(',') || [ 'http://localhost:5173', // Local dev 'http://localhost:51XX', // App's own web ]; app.enableCors({ origin: corsOrigins, credentials: true, }); await app.listen(process.env.PORT || 30XX); } ``` ### Template: docker-compose.staging.yml Entry ```yaml myproject-backend: image: ghcr.io/memo-2023/myproject-backend:${MYPROJECT_BACKEND_VERSION:-latest} container_name: myproject-backend-staging restart: unless-stopped ports: - '30XX:30XX' environment: NODE_ENV: production PORT: 30XX DATABASE_URL: postgresql://postgres:${POSTGRES_PASSWORD}@postgres:5432/myproject MANA_CORE_AUTH_URL: http://mana-core-auth:3001 # CORS - Include app's web AND manacore-web dashboard CORS_ORIGINS: http://46.224.108.214:51XX,http://46.224.108.214:5173,http://localhost:51XX,http://localhost:5173 depends_on: postgres: condition: service_healthy mana-core-auth: condition: service_healthy healthcheck: test: ['CMD', 'wget', '--no-verbose', '--tries=1', '--spider', 'http://localhost:30XX/api/v1/health'] interval: 30s timeout: 10s retries: 3 start_period: 40s networks: - manacore-network ``` --- ## 3. Deploying New Service to Staging ### Pre-Deployment Checklist - [ ] Database exists on staging PostgreSQL - [ ] Dockerfile has correct health check path (`/api/v1/health` for backends) - [ ] `docker-compose.staging.yml` has service definition - [ ] CORS_ORIGINS includes all required origins - [ ] Environment variables set correctly - [ ] Tag format matches project name exactly ### Create Database on Staging ```bash ssh -i ~/.ssh/hetzner_deploy_key deploy@46.224.108.214 # Create database docker exec manacore-postgres-staging psql -U postgres -c 'CREATE DATABASE myproject;' # Verify docker exec manacore-postgres-staging psql -U postgres -c '\l' | grep myproject ``` ### Deployment Tag Formats | Project | Correct Tag Format | Wrong Format | |---------|-------------------|--------------| | mana-core-auth | `mana-core-auth-staging-v1.0.X` | `auth-staging-v1.0.X` | | chat | `chat-staging-v1.0.X` or `chat-all-staging-v1.0.X` | - | | todo | `todo-staging-v1.0.X` or `todo-all-staging-v1.0.X` | - | | calendar | `calendar-staging-v1.0.X` | - | | clock | `clock-staging-v1.0.X` | - | | myproject | `myproject-staging-v1.0.X` | - | ### Post-Deployment Verification ```bash # Check container is running correct version docker ps --format '{{.Names}}: {{.Image}}' | grep myproject # Check health endpoint curl http://46.224.108.214:30XX/api/v1/health # Check logs for errors docker logs myproject-backend-staging --tail 50 # Test CORS (from manacore-web origin) curl -I -X OPTIONS http://46.224.108.214:30XX/api/v1/endpoint \ -H "Origin: http://46.224.108.214:5173" \ -H "Access-Control-Request-Method: GET" ``` --- ## 4. Adding Backend to ManaCore Dashboard When adding a new backend service that manacore-web dashboard should call: ### Checklist - [ ] Add CORS origin for manacore-web (port 5173) to backend - [ ] Create API service file in `manacore/apps/web/src/lib/api/services/` - [ ] Add runtime URL injection in `manacore/apps/web/src/hooks.server.ts` - [ ] Add environment variables to `docker-compose.staging.yml` for manacore-web - [ ] Deploy both manacore-web and the backend with new config ### Template: API Service File ```typescript // apps/manacore/apps/web/src/lib/api/services/myservice.ts import { browser } from '$app/environment'; import { createApiClient, type ApiResult } from '../base-client'; function getMyServiceApiUrl(): string { if (browser && typeof window !== 'undefined') { const injectedUrl = (window as unknown as { __PUBLIC_MYSERVICE_API_URL__?: string }) .__PUBLIC_MYSERVICE_API_URL__; if (injectedUrl) { return `${injectedUrl}/api/v1`; } } return 'http://localhost:30XX/api/v1'; } let _client: ReturnType | null = null; function getClient() { if (!_client) { _client = createApiClient(getMyServiceApiUrl()); } return _client; } // Export API functions export async function getItems(): Promise> { return getClient().get('/items'); } export async function createItem(data: CreateItemDto): Promise> { return getClient().post('/items', data); } ``` ### Template: hooks.server.ts Addition ```typescript // Add to existing hooks.server.ts const PUBLIC_MYSERVICE_API_URL_CLIENT = process.env.PUBLIC_MYSERVICE_API_URL_CLIENT || process.env.PUBLIC_MYSERVICE_API_URL || ''; // In transformPageChunk, add: window.__PUBLIC_MYSERVICE_API_URL__ = "${PUBLIC_MYSERVICE_API_URL_CLIENT}"; ``` ### Template: docker-compose.staging.yml Addition ```yaml manacore-web: environment: # ... existing env vars ... # Add new backend URL PUBLIC_MYSERVICE_API_URL: http://myservice-backend:30XX PUBLIC_MYSERVICE_API_URL_CLIENT: http://46.224.108.214:30XX ``` --- ## 5. Quick Reference Port Assignments ### Backend Ports (3000-3099) | Port | Service | |------|---------| | 3000 | chat-web (legacy) | | 3001 | mana-core-auth | | 3002 | chat-backend | | 3006 | picture-backend | | 3007 | zitare-backend | | 3009 | manadeck-backend | | 3015 | contacts-backend | | 3016 | calendar-backend | | 3017 | clock-backend | | 3018 | todo-backend | ### Web App Ports (5100-5199) | Port | Service | |------|---------| | 5173 | manacore-web | | 5175 | picture-web | | 5177 | zitare-web | | 5179 | calendar-web | | 5184 | contacts-web | | 5186 | calendar-web (staging) | | 5187 | clock-web | | 5188 | todo-web | ### Next Available Ports - **Backend**: 3019, 3020, 3021... - **Web**: 5189, 5190, 5191... --- ## Common Mistakes Quick Reference | Mistake | Fix | |---------|-----| | `import.meta.env` in Docker | Use `window.__PUBLIC_*__` injection | | API client at module level | Use lazy `getClient()` pattern | | `uuid` type for user_id | Use `text` type | | Missing CORS for 5173 | Add manacore-web to CORS_ORIGINS | | `auth-staging-v*` tag | Use `mana-core-auth-staging-v*` | | ALTER TABLE without USING | Use `USING column::text` | | `/api/health` endpoint | Use `/api/v1/health` |