feat: add CI/CD pipeline, production Dockerfile, and deployment architecture
- Add production Dockerfile with bun build --compile, multi-stage Alpine build - Add .dockerignore - Swap bcrypt -> bcryptjs (pure JS, no native addons) - Add programmatic migrations on startup via drizzle migrator - Add /v1/version endpoint with APP_VERSION baked in at build time - Add .gitea/workflows/ci.yml (lint + test with postgres/valkey services) - Add .gitea/workflows/build.yml (version bump, build, push to registry) - Update CLAUDE.md and docs/architecture.md to remove multi-tenancy - Add docs/deployment.md covering DOKS + ArgoCD architecture Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -56,11 +56,10 @@ src/
|
||||
### Request Flow
|
||||
|
||||
1. Fastify receives request
|
||||
2. `onRequest` hook sets `companyId` from header
|
||||
3. `authenticate` preHandler verifies JWT, loads permissions, checks `is_active`
|
||||
4. `requirePermission` preHandler checks user has required permission slug
|
||||
5. Route handler validates input with Zod, calls service, returns response
|
||||
6. Error handler catches typed errors and maps to HTTP status codes
|
||||
2. `authenticate` preHandler verifies JWT, loads permissions, checks `is_active`
|
||||
3. `requirePermission` preHandler checks user has required permission slug
|
||||
4. Route handler validates input with Zod, calls service, returns response
|
||||
5. Error handler catches typed errors and maps to HTTP status codes
|
||||
|
||||
### Permission Inheritance
|
||||
|
||||
@@ -109,9 +108,9 @@ src/
|
||||
| Theme | Zustand store, persisted to localStorage |
|
||||
| Component state | React `useState` |
|
||||
|
||||
## Multi-Tenancy
|
||||
## Deployment Model
|
||||
|
||||
Every domain table has a `company_id` column. All queries filter by the authenticated user's company. Location-scoped tables (inventory, transactions) additionally filter by `location_id`.
|
||||
Each customer runs as a fully isolated deployment — their own Kubernetes namespace on DOKS, their own database on the shared managed Postgres instance. There is no multi-tenancy in the application layer. No `company_id`, no row-level isolation. One instance = one customer.
|
||||
|
||||
## Database
|
||||
|
||||
|
||||
193
docs/deployment.md
Normal file
193
docs/deployment.md
Normal file
@@ -0,0 +1,193 @@
|
||||
# Infrastructure & Deployment Architecture
|
||||
|
||||
## Overview
|
||||
|
||||
LunarFront runs on DigitalOcean. Each customer is a fully isolated deployment — their own Kubernetes namespace, their own database. There is no multi-tenancy in the application layer.
|
||||
|
||||
**Stack:** DOKS (Kubernetes) · ArgoCD · Helm · Gitea · Terraform · Ansible
|
||||
|
||||
The guiding principle is simplicity — Docker Compose on Droplets for Gitea, Kubernetes for customer app instances. No surprise bills.
|
||||
|
||||
---
|
||||
|
||||
## Monthly Cost
|
||||
|
||||
| Resource | Spec | Cost |
|
||||
|---|---|---|
|
||||
| DOKS cluster | Control plane | $12/mo |
|
||||
| DOKS nodes (2x s-2vcpu-4gb) | Runs all customer apps | ~$48/mo |
|
||||
| Managed Postgres (shared) | All customer databases | $15-25/mo |
|
||||
| Managed Redis (shared) | All customer queues/cache | $15/mo |
|
||||
| Gitea Droplet | 1 vCPU, 1GB RAM | $6/mo |
|
||||
| Gitea Postgres | Dedicated managed Postgres | $15/mo |
|
||||
| Spaces | Registry, backups, files | ~$21/mo |
|
||||
| **Fixed total** | | **~$132-142/mo** |
|
||||
| **Per customer** | New database in shared Postgres | **~$0 marginal** |
|
||||
|
||||
---
|
||||
|
||||
## Environments
|
||||
|
||||
| Environment | Hostname | Purpose |
|
||||
|---|---|---|
|
||||
| Production (per customer) | `{customer}.app.lunarfront.tech` | Live customer instance on DOKS |
|
||||
| Gitea | `git.lunarfront.tech` | Source control, CI/CD, container registry |
|
||||
| Dev | Local / feature namespace on DOKS | Testing and staging |
|
||||
|
||||
---
|
||||
|
||||
## DNS
|
||||
|
||||
Managed through Cloudflare. Records defined in Terraform.
|
||||
|
||||
| Record | Type | Points To |
|
||||
|---|---|---|
|
||||
| `git.lunarfront.tech` | A | Gitea Droplet |
|
||||
| `git-ssh.lunarfront.tech` | A | Gitea Droplet (SSH port 2222) |
|
||||
| `registry.lunarfront.tech` | A | Gitea Droplet (container registry) |
|
||||
| `{customer}.app.lunarfront.tech` | CNAME | DOKS load balancer |
|
||||
|
||||
---
|
||||
|
||||
## Infrastructure (Terraform)
|
||||
|
||||
Terraform manages all DigitalOcean and Cloudflare resources. State stored in Spaces.
|
||||
|
||||
```
|
||||
/terraform
|
||||
main.tf providers, backend (Spaces state)
|
||||
variables.tf
|
||||
cluster.tf DOKS cluster + node pools
|
||||
databases.tf shared Postgres, shared Redis
|
||||
gitea.tf Gitea Droplet
|
||||
spaces.tf files, backups, tf-state buckets
|
||||
dns.tf Cloudflare DNS records
|
||||
outputs.tf cluster endpoint, DB URLs
|
||||
terraform.tfvars secrets (gitignored)
|
||||
```
|
||||
|
||||
### State Backend
|
||||
|
||||
```hcl
|
||||
terraform {
|
||||
backend "s3" {
|
||||
endpoint = "https://nyc3.digitaloceanspaces.com"
|
||||
bucket = "lunarfront-terraform-state"
|
||||
key = "terraform.tfstate"
|
||||
region = "us-east-1"
|
||||
skip_credentials_validation = true
|
||||
skip_metadata_api_check = true
|
||||
skip_region_validation = true
|
||||
force_path_style = true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Gitea (Source Control + CI/CD)
|
||||
|
||||
Gitea runs on its own Droplet with a dedicated managed Postgres. It hosts:
|
||||
- Source code repositories
|
||||
- Gitea Actions CI/CD pipelines
|
||||
- Docker container registry (`registry.lunarfront.tech`)
|
||||
|
||||
Managed by Ansible. See `lunarfront-infra/ansible/roles/gitea/`.
|
||||
|
||||
### CI Pipeline (Gitea Actions)
|
||||
|
||||
On push to `main`:
|
||||
|
||||
1. Run lint + unit tests using the shared `ci-runner` image
|
||||
2. Build Docker image, push to `registry.lunarfront.tech/ryan/lunarfront-app:{sha}`
|
||||
3. Update the Helm chart `values.yaml` with the new image tag
|
||||
4. ArgoCD detects the change and syncs all customer deployments
|
||||
|
||||
---
|
||||
|
||||
## Kubernetes (DOKS + ArgoCD)
|
||||
|
||||
All customer app instances run on a single DOKS cluster managed by ArgoCD (GitOps).
|
||||
|
||||
### Repository Structure
|
||||
|
||||
```
|
||||
/gitops
|
||||
apps/
|
||||
customer-acme/
|
||||
values.yaml
|
||||
customer-foo/
|
||||
values.yaml
|
||||
chart/
|
||||
Chart.yaml
|
||||
templates/
|
||||
deployment.yaml
|
||||
service.yaml
|
||||
ingress.yaml
|
||||
job-migrate.yaml # runs drizzle-kit migrate on deploy
|
||||
secret.yaml
|
||||
```
|
||||
|
||||
### Per-Customer values.yaml
|
||||
|
||||
```yaml
|
||||
customer: acme
|
||||
subdomain: acme
|
||||
|
||||
image:
|
||||
repository: registry.lunarfront.tech/ryan/lunarfront-app
|
||||
tag: "abc123"
|
||||
|
||||
database:
|
||||
url: "postgresql://lunarfront:pass@db-host:25060/customer_acme?sslmode=require"
|
||||
|
||||
redis:
|
||||
url: "rediss://..."
|
||||
|
||||
env:
|
||||
JWT_SECRET: "..."
|
||||
NODE_ENV: production
|
||||
```
|
||||
|
||||
### Adding a New Customer
|
||||
|
||||
1. `CREATE DATABASE customer_x;` on the shared managed Postgres
|
||||
2. Add `gitops/apps/customer-x/values.yaml`
|
||||
3. Push — ArgoCD syncs, migration Job runs, instance is live
|
||||
4. Add DNS CNAME in Terraform
|
||||
|
||||
---
|
||||
|
||||
## Database Strategy
|
||||
|
||||
One managed Postgres cluster shared across all customers. Each customer gets their own isolated database (`CREATE DATABASE customer_x`). No cross-customer queries are possible at the database level.
|
||||
|
||||
- New customer = new database, no new infrastructure cost
|
||||
- Managed DO Postgres handles backups, failover, and SSL
|
||||
- Resize the cluster as total load grows
|
||||
|
||||
---
|
||||
|
||||
## Day-to-Day Workflow
|
||||
|
||||
```bash
|
||||
# Provision infrastructure from scratch
|
||||
terraform init && terraform apply
|
||||
|
||||
# Configure Gitea server
|
||||
ansible-playbook ansible/gitea.yml -i ansible/inventory.ini
|
||||
|
||||
# Normal deploy flow — push to main
|
||||
git push origin main
|
||||
# → Gitea Actions: lint + test + build image
|
||||
# → Image pushed to registry
|
||||
# → Helm chart values updated
|
||||
# → ArgoCD syncs all customer deployments automatically
|
||||
|
||||
# Add a new customer
|
||||
# 1. Create database
|
||||
psql $DATABASE_URL -c "CREATE DATABASE customer_x;"
|
||||
# 2. Add values file, push
|
||||
git add gitops/apps/customer-x/values.yaml && git push
|
||||
# Done — ArgoCD handles the rest
|
||||
```
|
||||
Reference in New Issue
Block a user