diff --git a/docs/server-setup.md b/docs/server-setup.md new file mode 100644 index 0000000..3cbbea8 --- /dev/null +++ b/docs/server-setup.md @@ -0,0 +1,137 @@ +# Server Setup + +This document describes the requirements for hosting the BudgetWise monorepo deployments. It is intentionally tool-agnostic — any reverse proxy that meets the requirements below will work. + +--- + +## 1. SSH Deploy Access + +CI pipelines connect to the server over SSH to transfer built files. A dedicated deploy user is required. + +**Requirements:** +- A non-root system user (e.g. `deploy`) with shell access +- The CI deploy SSH public key added to that user's `~/.ssh/authorized_keys` +- SSH key must correspond to the `ssh_private_key` Woodpecker secret + +**Deploy directory:** +- The deploy user must have read/write access to the base deploy path (the value of the `deploy_path` Woodpecker secret, e.g. `/var/www/budget-app`) +- The directory must exist before the first pipeline run +- Subdirectories `production/` and `staging/` must also exist and be writable +- PR preview directories (`mr-{number}/`) are created and deleted automatically by the CI — no pre-creation needed + +``` +/var/www/budget-app/ ← deploy_path (must exist, writable by deploy user) + production/ ← must exist + staging/ ← must exist + mr-42/ ← created by CI on PR open, deleted on PR close + mr-91/ + ... +``` + +--- + +## 2. DNS + +Three types of hostnames are used: + +| Hostname pattern | Environment | Example | +|---|---|---| +| `{domain}` | Production | `budget.kushalgaywala.com` | +| `staging.{domain}` | Staging | `staging.budget.kushalgaywala.com` | +| `{pr-number}.{domain}` | PR preview | `42.budget.kushalgaywala.com` | + +**Requirements:** +- An A (or AAAA) record pointing `{domain}` to the server IP +- A wildcard A record `*.{domain}` pointing to the same server IP — this covers staging and all PR preview subdomains with a single record + +--- + +## 3. TLS + +**Requirements:** +- TLS certificates for `{domain}` and `*.{domain}` (wildcard) +- A wildcard certificate covers staging and all PR preview subdomains automatically, so no certificate management is needed per PR +- The wildcard certificate must be issued via a DNS-01 ACME challenge (HTTP-01 cannot validate wildcard domains) + +--- + +## 4. Reverse Proxy + +The reverse proxy sits in front of the deploy directories and routes incoming HTTPS requests to the correct static files. + +### 4.1 Static file serving + +Each environment maps a hostname to a directory of static HTML/JS/CSS files built by Vite. + +| Hostname | Directory | +|---|---| +| `{domain}` | `{deploy_path}/production/` | +| `staging.{domain}` | `{deploy_path}/staging/` | +| `{pr-number}.{domain}` | `{deploy_path}/mr-{pr-number}/` | + +### 4.2 SPA fallback routing + +The app is a single-page application (React Router). Any request for a path that does not match a physical file must fall back to serving `index.html` from that environment's directory. + +**Example:** A request for `/buckets/42` has no corresponding file on disk. The proxy must serve `index.html` instead of returning a 404, so React Router can handle the route client-side. + +**Requirement:** For every environment, configure: "if the requested file does not exist, serve `index.html` from the same root directory." + +### 4.3 Wildcard PR preview routing + +The proxy must extract the PR number from the subdomain and map it to the correct directory dynamically — without needing a config change per PR. + +**Requirement:** Match hostnames of the form `{number}.{domain}` and serve files from `{deploy_path}/mr-{number}/`, with the SPA fallback above applied. + +**Note:** If a PR preview directory does not exist (e.g. the PR was closed and cleaned up), the proxy should return a 404 or a friendly error page rather than serving files from another environment. + +### 4.4 HTTP → HTTPS redirect + +**Requirement:** All plain HTTP requests must be redirected to HTTPS (301 or 308). + +### 4.5 Headers (recommended) + +| Header | Value | Reason | +|---|---|---| +| `X-Frame-Options` | `DENY` | Prevent clickjacking | +| `X-Content-Type-Options` | `nosniff` | Prevent MIME sniffing | +| `Referrer-Policy` | `strict-origin-when-cross-origin` | Limit referrer leakage | +| `Cache-Control` for `index.html` | `no-cache` | Ensure users get the latest app shell | +| `Cache-Control` for `assets/` | `public, max-age=31536000, immutable` | Vite hashes asset filenames, safe to cache forever | + +--- + +## 5. Woodpecker CI + +Woodpecker must be connected to the Gitea repo and have the following secrets configured under **Repository → Settings → Secrets**: + +| Secret name | Description | +|---|---| +| `ssh_private_key` | Private SSH key for the deploy user | +| `deploy_host` | Server hostname or IP | +| `deploy_user` | SSH username on the server | +| `deploy_path` | Absolute base path, e.g. `/var/www/budget-app` | +| `prod_url` | Full production URL, e.g. `https://budget.kushalgaywala.com` | +| `prod_appwrite_endpoint` | Production Appwrite API endpoint | +| `prod_appwrite_project_id` | Production Appwrite project ID | +| `prod_appwrite_api_key` | Production Appwrite API key (`databases.write` + `collections.write` scopes) | +| `staging_url` | Full staging URL, e.g. `https://staging.budget.kushalgaywala.com` | +| `staging_appwrite_endpoint` | Staging Appwrite API endpoint | +| `staging_appwrite_project_id` | Staging Appwrite project ID | +| `staging_appwrite_api_key` | Staging Appwrite API key (same scopes) | +| `base_domain` | Base domain for PR previews, e.g. `budget.kushalgaywala.com` | + +--- + +## 6. What CI Handles Automatically + +Once the above is in place, no further manual server changes are needed for day-to-day use: + +| Event | What happens automatically | +|---|---| +| Push to `main` | Web app built with production credentials and rsynced to `production/` | +| Push to `staging` | Web app built with staging credentials and rsynced to `staging/` | +| PR opened / updated | Web app built and deployed to `mr-{number}/` | +| PR closed | `mr-{number}/` directory removed from server | +| `apps/appwrite/**` changed on `main` | Appwrite schema deployed to production Appwrite project | +| `apps/appwrite/**` changed on `staging` | Appwrite schema deployed to staging Appwrite project |