# 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` |