Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
4742308
feat: add Helm chart and Dockerfile for deploying Latitude to Kubernetes
claude Mar 6, 2026
0881d91
build: add build scripts to all packages for Docker bundling
geclos Mar 16, 2026
1ec067b
build: add tsup bundling for api, ingest, and workers apps
geclos Mar 16, 2026
486128c
fix: handle import.meta.url for CJS bundles in server entry points
geclos Mar 16, 2026
b385015
fix: use LAT_ prefixed env vars for ClickHouse client
geclos Mar 16, 2026
666ee74
build: update Dockerfile for production bundling with tsup
geclos Mar 16, 2026
0ab2819
chore: update pnpm-lock.yaml with tsup dependency
geclos Mar 16, 2026
f37e790
build: pin curl version in Dockerfile for hadolint compliance
geclos Mar 16, 2026
50211a8
build: add SHELL directive with pipefail for hadolint compliance
geclos Mar 16, 2026
af256a0
chore: update ClickHouse env vars to use LAT_ prefix in .env.example
geclos Mar 16, 2026
47a0671
chore: add both CLICKHOUSE_* and LAT_CLICKHOUSE_* env vars in .env.ex…
geclos Mar 16, 2026
071fcdb
chore: reorganize ClickHouse env vars in .env.example
geclos Mar 16, 2026
a39d3ac
fix: ch env vars prefixing
geclos Mar 16, 2026
5020aae
docs: add README for kind local testing environment
geclos Mar 16, 2026
37b82fd
fix: use /api/health endpoint for web service health checks
geclos Mar 16, 2026
13f7a7c
fix: use custom server entry for web app instead of Nitro
geclos Mar 16, 2026
bf2cf3d
feat: replaced custom server with nitro for production web build
geclos Mar 16, 2026
68fa231
fix: simplify docker images and align migration env config
geclos Mar 16, 2026
8d0249f
feat: add bundle analyzer and split output build in vendor chunks for…
geclos Mar 16, 2026
e13fa53
ci: enforce web bundle size limit on main PRs
geclos Mar 16, 2026
3b649f3
remove slate json
geclos Mar 16, 2026
6d9d74c
fix typo
geclos Mar 16, 2026
7060793
chore: add missing tsc build scripts to shared packages
geclos Mar 16, 2026
5e4f3a3
fix: run web bundle size check with production env
geclos Mar 16, 2026
25157fb
fix: address PR review findings
cursoragent Mar 16, 2026
ca838e0
fix: add @platform/env dependency to workers app
cursoragent Mar 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 14 additions & 10 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
**/.git/
**/.next/
**/dist/
.git/
.github/
.husky/
.cursor/
.turbo/
.pnpm-store/
**/node_modules/
**/docker/
**/dist/
**/coverage/
**/.turbo/
**/.next/
docker/
charts/
*.md
.git
.pnpm-store
Dockerfile
docker
node_modules
npm-debug.log
out
.env*
!.env.example
9 changes: 8 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ POSTGRES_RUNTIME_USER=latitude_app
POSTGRES_RUNTIME_PASSWORD=secret

# ClickHouse
CLICKHOUSE_URL=http://localhost:8123
CLICKHOUSE_USER=latitude
CLICKHOUSE_PASSWORD=secret
CLICKHOUSE_DB=latitude_development
Expand Down Expand Up @@ -66,6 +65,12 @@ LAT_WEAVIATE_GRPC_PORT=50051
LAT_REDIS_HOST=localhost
LAT_REDIS_PORT=6379

# ClickHouse
LAT_CLICKHOUSE_URL=http://localhost:8123
LAT_CLICKHOUSE_USER=latitude
LAT_CLICKHOUSE_PASSWORD=secret
LAT_CLICKHOUSE_DB=latitude_development

# Redpanda/Kafka Configuration
LAT_KAFKA_BROKERS=localhost:9092
LAT_KAFKA_CLIENT_ID=latitude-app
Expand Down Expand Up @@ -96,6 +101,7 @@ LAT_API_URL=http://localhost:3001
LAT_API_PORT=3001
LAT_INGEST_URL=http://localhost:3002
LAT_INGEST_PORT=3002
# LAT_WORKERS_HEALTH_PORT=9090

# API Key Encryption (32-byte hex key for AES-256-GCM)
LAT_API_KEY_ENCRYPTION_KEY=75d697b90c1e46c13bd7f7343ab2b9a9e430cdcda05d47f055e1523d54d5409b
Expand Down Expand Up @@ -146,5 +152,6 @@ LAT_MAILPIT_FROM=noreply@latitude.local
# LAT_STRIPE_WEBHOOK_SECRET=whsec_xxx

# Frontend
# LAT_WEB_BUNDLE_ANALYZE=false
VITE_LAT_API_URL=http://localhost:3001/v1
VITE_LAT_WEB_URL=http://localhost:3000
37 changes: 37 additions & 0 deletions .github/workflows/web-bundle-size.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: Web Bundle Size

on:
pull_request:
branches:
- main

jobs:
web-bundle-size:
runs-on: ubuntu-latest

env:
NODE_ENV: production

steps:
- name: Checkout repository
uses: actions/checkout@v5

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 25.x

- name: Install pnpm
uses: pnpm/action-setup@v4

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Create .env.production from example
run: cp .env.example .env.production

- name: Build web app
run: pnpm --filter @app/web build

- name: Enforce client bundle size limit
run: pnpm --filter @app/web check:bundle-size
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -129,3 +129,4 @@ PRD.md
progress.txt

packages/cli/prompts
.output
224 changes: 224 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
# syntax=docker/dockerfile:1

# ---------------------------------------------------------------------------
# Base image — shared by all stages
# ---------------------------------------------------------------------------
FROM node:25-slim AS base

# Install pnpm using npm (corepack was removed from Node.js 25)
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates=202* && \
npm install -g pnpm@10.30.3 && \
rm -rf /var/lib/apt/lists/*

WORKDIR /app

# Enable pipefail for proper error handling in piped commands
SHELL ["/bin/bash", "-o", "pipefail", "-c"]

# ---------------------------------------------------------------------------
# Prefetch dependencies into pnpm store (cache-friendly)
# ---------------------------------------------------------------------------
FROM base AS deps

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

# Populate pnpm store from lockfile only for better cache reuse
RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store \
pnpm fetch --frozen-lockfile --ignore-scripts

# ---------------------------------------------------------------------------
# Source — full repo with deps installed
# ---------------------------------------------------------------------------
FROM deps AS source

COPY . .

# Skip postinstall scripts (chdb and other dev-only native deps)
RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store \
pnpm install --frozen-lockfile --ignore-scripts --offline

# ---------------------------------------------------------------------------
# Build api — compile api app (turbo builds dependencies automatically)
# ---------------------------------------------------------------------------
FROM source AS build-api

RUN pnpm --filter @app/api build

# ---------------------------------------------------------------------------
# Build ingest — compile ingest app (turbo builds dependencies automatically)
# ---------------------------------------------------------------------------
FROM source AS build-ingest

RUN pnpm --filter @app/ingest build

# ---------------------------------------------------------------------------
# Build workers — compile workers app (turbo builds dependencies automatically)
# ---------------------------------------------------------------------------
FROM source AS build-workers

RUN pnpm --filter @app/workers build

# ---------------------------------------------------------------------------
# Build web — compile web app (turbo builds dependencies automatically)
# ---------------------------------------------------------------------------
FROM source AS build-web

ARG VITE_LAT_API_URL
ARG VITE_LAT_WEB_URL

RUN pnpm --filter @app/web build

# ---------------------------------------------------------------------------
# Build migrations — compile packages needed for migrations
# ---------------------------------------------------------------------------
FROM source AS build-migrations

RUN pnpm --filter @platform/db-postgres build && \
pnpm --filter @platform/db-clickhouse build && \
pnpm --filter @platform/db-weaviate build

# ---------------------------------------------------------------------------
# Runtime base — shared runtime settings and cleanup helper
# ---------------------------------------------------------------------------
FROM base AS runtime

ENV NODE_ENV=production

RUN groupadd -r latitude && useradd -r -g latitude -d /app -s /sbin/nologin latitude && \
chown -R latitude:latitude /app

RUN cat <<'EOF' > /usr/local/bin/prune-workspace && chmod +x /usr/local/bin/prune-workspace
#!/bin/bash
set -euo pipefail

find packages -name "src" -type d -exec rm -rf {} + 2>/dev/null || true
find packages -name "*.ts" -not -path "*/node_modules/*" -not -name "*.d.ts" -delete
find packages -name "tsconfig.json" -delete
find . -name "*.test.ts" -delete
find . -name "*.spec.ts" -delete
EOF

RUN cat <<'EOF' > /usr/local/bin/install-prod-deps && chmod +x /usr/local/bin/install-prod-deps
#!/bin/bash
set -euo pipefail

pnpm install --frozen-lockfile --ignore-scripts --production
EOF

# ---------------------------------------------------------------------------
# Target: api — minimal image with only api app
# ---------------------------------------------------------------------------
FROM runtime AS api

COPY --from=build-api /app/apps/api/dist ./apps/api/dist
COPY --from=build-api /app/apps/api/package.json ./apps/api/package.json
COPY --from=build-api /app/package.json ./package.json
COPY --from=build-api /app/pnpm-lock.yaml ./pnpm-lock.yaml
COPY --from=build-api /app/pnpm-workspace.yaml ./pnpm-workspace.yaml
COPY --from=build-api /app/packages ./packages

RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store \
install-prod-deps

RUN prune-workspace
USER latitude
EXPOSE 3001

CMD ["node", "apps/api/dist/server.cjs"]

# ---------------------------------------------------------------------------
# Target: ingest — minimal image with only ingest app
# ---------------------------------------------------------------------------
FROM runtime AS ingest

COPY --from=build-ingest /app/apps/ingest/dist ./apps/ingest/dist
COPY --from=build-ingest /app/apps/ingest/package.json ./apps/ingest/package.json
COPY --from=build-ingest /app/package.json ./package.json
COPY --from=build-ingest /app/pnpm-lock.yaml ./pnpm-lock.yaml
COPY --from=build-ingest /app/pnpm-workspace.yaml ./pnpm-workspace.yaml
COPY --from=build-ingest /app/packages ./packages

RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store \
install-prod-deps

RUN prune-workspace
USER latitude
EXPOSE 3002

CMD ["node", "apps/ingest/dist/server.cjs"]

# ---------------------------------------------------------------------------
# Target: workers — minimal image with only workers app
# ---------------------------------------------------------------------------
FROM runtime AS workers

COPY --from=build-workers /app/apps/workers/dist ./apps/workers/dist
COPY --from=build-workers /app/apps/workers/package.json ./apps/workers/package.json
COPY --from=build-workers /app/package.json ./package.json
COPY --from=build-workers /app/pnpm-lock.yaml ./pnpm-lock.yaml
COPY --from=build-workers /app/pnpm-workspace.yaml ./pnpm-workspace.yaml
COPY --from=build-workers /app/packages ./packages

RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store \
install-prod-deps

RUN prune-workspace
USER latitude
EXPOSE 9090

CMD ["node", "apps/workers/dist/server.cjs"]

# ---------------------------------------------------------------------------
# Target: web — minimal image with only web app (TanStack Start SSR with Nitro)
# ---------------------------------------------------------------------------
FROM runtime AS web

COPY --from=build-web /app/apps/web/.output ./apps/web/.output
COPY --from=build-web /app/apps/web/package.json ./apps/web/package.json
COPY --from=build-web /app/package.json ./package.json
COPY --from=build-web /app/pnpm-lock.yaml ./pnpm-lock.yaml
COPY --from=build-web /app/pnpm-workspace.yaml ./pnpm-workspace.yaml
COPY --from=build-web /app/packages ./packages

RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store \
install-prod-deps

RUN prune-workspace
USER latitude
EXPOSE 3000

CMD ["node", "apps/web/.output/server/index.mjs"]

# ---------------------------------------------------------------------------
# Target: migrations — minimal image with migration tools
# ---------------------------------------------------------------------------
FROM runtime AS migrations

# Install curl and goose for ClickHouse migrations
# hadolint ignore=DL3008
RUN apt-get update && \
apt-get install -y --no-install-recommends curl && \
GOOSE_VERSION=3.24.1 && \
ARCH=$(dpkg --print-architecture) && \
case "$ARCH" in \
amd64) GOOSE_ARCH="x86_64" ;; \
arm64) GOOSE_ARCH="aarch64" ;; \
*) GOOSE_ARCH="$ARCH" ;; \
esac && \
curl -fsSL "https://github.com/pressly/goose/releases/download/v${GOOSE_VERSION}/goose_linux_${GOOSE_ARCH}" \
-o /usr/local/bin/goose && \
chmod +x /usr/local/bin/goose && \
apt-get purge -y curl && apt-get autoremove -y && rm -rf /var/lib/apt/lists/*

COPY --from=build-migrations /app/packages ./packages
COPY --from=build-migrations /app/apps/workflows ./apps/workflows
COPY --from=build-migrations /app/package.json ./package.json
COPY --from=build-migrations /app/pnpm-lock.yaml ./pnpm-lock.yaml
COPY --from=build-migrations /app/pnpm-workspace.yaml ./pnpm-workspace.yaml

RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store \
pnpm install --frozen-lockfile --ignore-scripts

USER latitude

CMD ["sh", "-c", "pnpm --filter @platform/db-postgres pg:migrate && pnpm --filter @platform/db-clickhouse ch:up && pnpm --filter @platform/db-weaviate wv:migrate"]
5 changes: 3 additions & 2 deletions apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"private": true,
"type": "module",
"scripts": {
"build": "tsc -p tsconfig.json",
"build": "tsup",
"dev": "tsx watch src/server.ts",
"check": "biome check src",
"format": "biome format --write src",
Expand Down Expand Up @@ -38,6 +38,7 @@
},
"devDependencies": {
"@platform/testkit": "workspace:*",
"@repo/vitest-config": "workspace:*"
"@repo/vitest-config": "workspace:*",
"tsup": "^8.0.0"
}
}
8 changes: 3 additions & 5 deletions apps/api/src/middleware/touch-buffer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ interface TouchBufferConfig {
*
* Performance impact:
* - Reduces writes by 90%+ (30s window / avg 100ms request = 300:1 reduction)
* - lastUsedAt accuracy reduced to flush interval (±30 seconds by default)
* - lastUsedAt accuracy reduced to flush interval (+/-30 seconds by default)
*
* Usage:
* ```typescript
Expand Down Expand Up @@ -111,11 +111,9 @@ class TouchBuffer {
}).pipe(Effect.provide(apiKeyRepoLayer), Effect.provide(sqlClientLayer)),
)

const duration = Date.now() - startTime
logger.info(`Flushed ${keyIds.length} touch updates in ${duration}ms`)
logger.info(`Flushed ${keyIds.length} touch updates in ${Date.now() - startTime}ms`)
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error"
logger.error(`Failed to flush touch updates: ${errorMessage}`)
logger.error(`Failed to flush touch updates: ${error instanceof Error ? error.message : "Unknown error"}`)

// Re-add failed keys to buffer for retry (with limit to prevent unbounded growth)
for (const [keyId, timestamp] of batch) {
Expand Down
2 changes: 1 addition & 1 deletion apps/api/src/routes/health.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ describe("GET /health", () => {
})

describe("with database connections", () => {
// These tests require LAT_DATABASE_URL and CLICKHOUSE_URL to be set
// These tests require LAT_DATABASE_URL and LAT_CLICKHOUSE_URL to be set
// Run with: pnpm --filter @app/api test
// The .env.test file at repo root is automatically loaded by vitest config

Expand Down
7 changes: 5 additions & 2 deletions apps/api/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,11 @@ import { registerRoutes } from "./routes/index.ts"
import { logger } from "./utils/logger.ts"

const nodeEnv = process.env.NODE_ENV || "development"
const envFilePath = fileURLToPath(new URL(`../../../.env.${nodeEnv}`, import.meta.url))
if (existsSync(envFilePath)) loadDotenv({ path: envFilePath, quiet: true })
// Load .env file for local development; skipped in production containers where the file won't exist
if (import.meta.url) {
const envFilePath = fileURLToPath(new URL(`../../../.env.${nodeEnv}`, import.meta.url))
if (existsSync(envFilePath)) loadDotenv({ path: envFilePath, quiet: true })
}

const app = new OpenAPIHono()
const port = Effect.runSync(parseEnv("LAT_API_PORT", "number", 3001))
Expand Down
Loading
Loading