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