From 6a48a6b9c8224e0d3fc61ace5cae91808d3b497f Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 21 Jun 2026 14:28:18 +0000 Subject: [PATCH 1/4] feat: add Dockerfile, release workflow, and API docs - Multi-stage Dockerfile (node:22-slim) with a non-root user and a /app/data volume for the SQLite DB and attachments - .dockerignore to keep the build context lean - .github/workflows/release.yml: bumps semver on every push to main using conventional commits, pushes Docker image to ghcr.io, and creates a GitHub release - docs/api.md: API reference and permissions model moved out of README - README trimmed to quick-start, Docker usage, configuration, and links --- .dockerignore | 10 ++++ .github/workflows/release.yml | 101 ++++++++++++++++++++++++++++++++++ Dockerfile | 30 ++++++++++ README.md | 98 ++++++++------------------------- docs/api.md | 67 ++++++++++++++++++++++ 5 files changed, 232 insertions(+), 74 deletions(-) create mode 100644 .dockerignore create mode 100644 .github/workflows/release.yml create mode 100644 Dockerfile create mode 100644 docs/api.md diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..7209bae --- /dev/null +++ b/.dockerignore @@ -0,0 +1,10 @@ +node_modules +dist +.git +.github +tests +*.db +attachments/ +.env +.env.local +coverage/ diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..6109b91 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,101 @@ +name: Release + +# Runs on every push to main except the version-bump commit this workflow +# produces, preventing an infinite loop. +on: + push: + branches: [main] + +jobs: + release: + if: "!startsWith(github.event.head_commit.message, 'chore: release')" + runs-on: ubuntu-latest + permissions: + contents: write # push version bump + tag, create release + packages: write # push Docker image to ghcr.io + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # full history so we can inspect commits since last tag + + - uses: pnpm/action-setup@v4 + with: + version: 10 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + + - run: pnpm install --frozen-lockfile + + # Inspect commit messages since the last tag (or all commits if no tag + # yet) and pick a semver bump level using conventional commits: + # BREAKING CHANGE or ! suffix → major + # feat: → minor + # anything else → patch + - name: Determine version bump + id: semver + run: | + LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") + + if [ -z "$LATEST_TAG" ]; then + COMMITS=$(git log --pretty=format:"%s%n%b") + else + COMMITS=$(git log "${LATEST_TAG}..HEAD" --pretty=format:"%s%n%b") + fi + + BUMP="patch" + while IFS= read -r line; do + if echo "$line" | grep -qE "BREAKING[- ]CHANGE|^[a-z]+(\(.+\))?!:"; then + BUMP="major" + break + elif echo "$line" | grep -qE "^feat(\(.+\))?:"; then + if [ "$BUMP" = "patch" ]; then BUMP="minor"; fi + fi + done <<< "$COMMITS" + + echo "bump=$BUMP" >> "$GITHUB_OUTPUT" + + - name: Bump version in package.json + id: version + run: | + NEW_VERSION=$(npm version ${{ steps.semver.outputs.bump }} --no-git-tag-version) + echo "new_version=$NEW_VERSION" >> "$GITHUB_OUTPUT" + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push Docker image + uses: docker/build-push-action@v6 + with: + context: . + push: true + tags: | + ghcr.io/${{ github.repository }}:${{ steps.version.outputs.new_version }} + ghcr.io/${{ github.repository }}:latest + + # Commit the package.json bump, tag it, and push. Requires that the + # github-actions bot is allowed to push to main (configure via branch + # protection → "Allow specific actors to bypass required pull requests"). + - name: Commit, tag, and push + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add package.json + git commit -m "chore: release ${{ steps.version.outputs.new_version }}" + git tag "${{ steps.version.outputs.new_version }}" + git push origin main --follow-tags + + - name: Create GitHub release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh release create "${{ steps.version.outputs.new_version }}" \ + --title "${{ steps.version.outputs.new_version }}" \ + --generate-notes diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..51d0666 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,30 @@ +# deps: install production dependencies only +FROM node:22-slim AS deps +RUN npm install -g pnpm@10 +WORKDIR /app +COPY package.json pnpm-lock.yaml ./ +RUN pnpm install --frozen-lockfile --prod + +# builder: compile TypeScript +FROM node:22-slim AS builder +RUN npm install -g pnpm@10 +WORKDIR /app +COPY package.json pnpm-lock.yaml tsconfig.json tsconfig.build.json ./ +RUN pnpm install --frozen-lockfile +COPY src ./src +RUN pnpm build + +# runner: minimal production image +FROM node:22-slim AS runner +RUN groupadd -r app && useradd -r -g app app +WORKDIR /app +COPY --from=deps --chown=app:app /app/node_modules ./node_modules +COPY --from=builder --chown=app:app /app/dist ./dist +COPY --from=builder --chown=app:app /app/package.json ./ +RUN mkdir -p /app/data && chown app:app /app/data +VOLUME ["/app/data"] +ENV NODE_ENV=production +ENV DB_PATH=/app/data/stack.db +EXPOSE 3000 +USER app +CMD ["node", "dist/index.js"] diff --git a/README.md b/README.md index 09dcc77..7e515f5 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,21 @@ The server listens on `PORT` (default `3000`). On first run it initializes a new --- +## Docker + +```sh +docker run -d \ + -e OWNER_TOKEN= \ + -e ENTITY_ID= \ + -p 3000:3000 \ + -v haverstack-data:/app/data \ + ghcr.io/haverstack/server:latest +``` + +`/app/data` holds the SQLite database and attachments — mount a volume there for persistence. Set `ENTITY_ID` only on first run; it is ignored once the database exists. + +--- + ## Configuration All configuration is via environment variables. See `.env.example` for the full list. @@ -29,87 +44,15 @@ All configuration is via environment variables. See `.env.example` for the full | ---------------------- | --------- | ------------------ | -------------------------------------------------------- | | `OWNER_TOKEN` | Yes | — | Bearer token for the stack owner. Treat like a password. | | `ENTITY_ID` | First run | — | Owner entity ID. Only needed when initializing a new DB. | -| `DB_PATH` | No | `./stack.db` | Path to the SQLite database file. | +| `DB_PATH` | No | `/app/data/stack.db` (Docker) / `./stack.db` | Path to the SQLite database file. | | `PORT` | No | `3000` | Port to listen on. | | `TIMEZONE` | No | `UTC` | IANA timezone. Only used on first run. | -| `CORS_ORIGINS` | No | `*` | Allowed origins, comma-separated or `*`. | +| `CORS_ORIGINS` | No | `` (none) | Allowed origins, comma-separated or `*`. | | `BASE_URL` | No | auto-detected | Canonical base URL of this server. | | `MAX_ATTACHMENT_BYTES` | No | `52428800` (50 MB) | Maximum attachment upload size. | --- -## API - -All routes are prefixed by the base URL. Requests are authenticated with a `Bearer` token in the `Authorization` header. - -### Discovery - -| Method | Path | Auth | Description | -| ------ | -------------------- | ---- | ------------------------------- | -| GET | `/.well-known/stack` | None | Stack metadata and capabilities | -| GET | `/health` | None | Liveness check | - -### Records - -| Method | Path | Auth | Description | -| ------ | -------------------------------- | -------- | --------------------------------------- | -| GET | `/records` | Optional | Query records via URL params | -| POST | `/records/query` | Optional | Query records with content filters | -| POST | `/records` | Required | Create a record | -| GET | `/records/:id` | Optional | Get a record by ID | -| PATCH | `/records/:id` | Required | Update record content (merge patch) | -| DELETE | `/records/:id` | Required | Soft-delete (or hard with `?hard=true`) | -| GET | `/records/:id/permissions` | Optional | Get permissions | -| PUT | `/records/:id/permissions` | Required | Replace permissions | -| GET | `/records/:id/associations` | Optional | List associations | -| POST | `/records/:id/associations` | Required | Add an association | -| DELETE | `/records/:id/associations` | Required | Remove an association | -| GET | `/records/:id/versions` | Optional | List version history | -| GET | `/records/:id/versions/:version` | Optional | Get a specific version | -| POST | `/records/:id/restore/:version` | Required | Restore a previous version | - -### Types - -| Method | Path | Auth | Description | -| ------ | ------------ | ---------- | ------------------------- | -| GET | `/types` | None | List all registered types | -| GET | `/types/:id` | None | Get a type by ID | -| POST | `/types` | Owner only | Register a new type | - -### Attachments - -| Method | Path | Auth | Description | -| ------ | ---------------------- | ---------- | --------------- | -| POST | `/attachments` | Required | Upload a file | -| GET | `/attachments/:fileId` | Optional | Download a file | -| DELETE | `/attachments/:fileId` | Owner only | Delete a file | - -### Entity & Tokens - -| Method | Path | Auth | Description | -| ------ | ------------- | ---------- | ------------------------------ | -| GET | `/entity` | Required | Get the owner entity record | -| PATCH | `/entity` | Owner only | Update the owner entity record | -| GET | `/tokens` | Owner only | List API tokens | -| POST | `/tokens` | Owner only | Create an API token | -| DELETE | `/tokens/:id` | Owner only | Revoke an API token | - ---- - -## Permissions - -Records are private by default (readable only by the stack owner). The `permissions` field controls access: - -```json -{ "access": "public" } -{ "access": "entity", "entityId": "...", "read": true, "write": false } -{ "access": "group", "groupId": "...", "read": true, "write": true } -``` - -Non-owner entities authenticate with tokens issued via `POST /tokens` and are subject to both record-level permissions and create-grant checks on write. - ---- - ## Development ```sh @@ -123,6 +66,13 @@ pnpm format:check # Check formatting --- +## Docs + +- [API reference](./docs/api.md) — routes, auth, and permissions +- [Deployment guide](./docs/deployment.md) — TLS, CORS, and rate limiting + +--- + ## Related - [`haverstack/core`](https://github.com/haverstack/core) — core library, types, adapters, and spec diff --git a/docs/api.md b/docs/api.md new file mode 100644 index 0000000..c50bb63 --- /dev/null +++ b/docs/api.md @@ -0,0 +1,67 @@ +# API Reference + +All routes are prefixed by the base URL. Requests are authenticated with a `Bearer` token in the `Authorization` header. + +## Discovery + +| Method | Path | Auth | Description | +| ------ | -------------------- | ---- | ------------------------------- | +| GET | `/.well-known/stack` | None | Stack metadata and capabilities | +| GET | `/health` | None | Liveness check | + +## Records + +| Method | Path | Auth | Description | +| ------ | -------------------------------- | -------- | --------------------------------------- | +| GET | `/records` | Optional | Query records via URL params | +| POST | `/records/query` | Optional | Query records with content filters | +| POST | `/records` | Required | Create a record | +| GET | `/records/:id` | Optional | Get a record by ID | +| PATCH | `/records/:id` | Required | Update record content (merge patch) | +| DELETE | `/records/:id` | Required | Soft-delete (or hard with `?hard=true`) | +| GET | `/records/:id/permissions` | Optional | Get permissions | +| PUT | `/records/:id/permissions` | Required | Replace permissions | +| GET | `/records/:id/associations` | Optional | List associations | +| POST | `/records/:id/associations` | Required | Add an association | +| DELETE | `/records/:id/associations` | Required | Remove an association | +| GET | `/records/:id/versions` | Optional | List version history | +| GET | `/records/:id/versions/:version` | Optional | Get a specific version | +| POST | `/records/:id/restore/:version` | Required | Restore a previous version | + +## Types + +| Method | Path | Auth | Description | +| ------ | ------------ | ---------- | ------------------------- | +| GET | `/types` | None | List all registered types | +| GET | `/types/:id` | None | Get a type by ID | +| POST | `/types` | Owner only | Register a new type | + +## Attachments + +| Method | Path | Auth | Description | +| ------ | ---------------------- | ---------- | --------------- | +| POST | `/attachments` | Required | Upload a file | +| GET | `/attachments/:fileId` | Optional | Download a file | +| DELETE | `/attachments/:fileId` | Owner only | Delete a file | + +## Entity & Tokens + +| Method | Path | Auth | Description | +| ------ | ------------- | ---------- | ------------------------------ | +| GET | `/entity` | Required | Get the owner entity record | +| PATCH | `/entity` | Owner only | Update the owner entity record | +| GET | `/tokens` | Owner only | List API tokens | +| POST | `/tokens` | Owner only | Create an API token | +| DELETE | `/tokens/:id` | Owner only | Revoke an API token | + +## Permissions + +Records are private by default (readable only by the stack owner). The `permissions` field controls access: + +```json +{ "access": "public" } +{ "access": "entity", "entityId": "...", "read": true, "write": false } +{ "access": "group", "groupId": "...", "read": true, "write": true } +``` + +Non-owner entities authenticate with tokens issued via `POST /tokens` and are subject to both record-level permissions and create-grant checks on write. From 767656a3397d34d462d2d8116ac65153f175bb4f Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 21 Jun 2026 14:32:56 +0000 Subject: [PATCH 2/4] docs: add CONTRIBUTING.md with conventional commits guide --- CONTRIBUTING.md | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..b02e5ca --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,42 @@ +# Contributing + +## Development setup + +```sh +cp .env.example .env +# Edit .env — set OWNER_TOKEN and ENTITY_ID at minimum +pnpm install +pnpm dev +``` + +Run the full check suite before opening a PR: + +```sh +pnpm typecheck +pnpm lint +pnpm format:check +pnpm test +``` + +All four run in CI and failures block merge. + +## Commit style + +This project uses [Conventional Commits](https://www.conventionalcommits.org/). Every commit message must start with a type prefix. The release workflow reads commit messages since the last tag to determine the version bump automatically: + +| Commit type | Example | Version bump | +| --- | --- | --- | +| `feat:` | `feat: add token expiry` | minor | +| `feat!:` or `BREAKING CHANGE` footer | `feat!: rename /entity to /owner` | major | +| Anything else (`fix:`, `chore:`, `docs:`, `refactor:`, `test:`, `perf:`) | `fix: return 404 on missing record` | patch | + +A scope is optional: `fix(attachments): reject empty filename`. + +Getting the type wrong produces the wrong version bump with no warning, so when in doubt use `fix:` for patches and `feat:` only for genuinely new capabilities. + +## Pull requests + +- One concern per PR. Split unrelated changes. +- The PR description should explain *why*, not just *what* — the diff already shows what changed. +- Link to any relevant issue with `Closes #N`. +- Keep PRs small enough to review in one sitting; large refactors are fine but flag them early. From c896a751fbc659003fbe23fbdde0671a107df311 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 21 Jun 2026 14:38:23 +0000 Subject: [PATCH 3/4] docs: expand deployment guide with nginx, Caddy, and Traefik sections --- docs/deployment.md | 152 ++++++++++++++++++++++++++++++++++++--------- 1 file changed, 124 insertions(+), 28 deletions(-) diff --git a/docs/deployment.md b/docs/deployment.md index 8b2492c..f47f628 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -1,54 +1,150 @@ # Deployment Guide -## TLS / HTTPS +The server speaks plain HTTP. In production, always place it behind a reverse proxy that handles TLS termination and rate limiting. Never expose the plain-HTTP port directly to the internet. -The server speaks plain HTTP. In production, always place it behind a reverse proxy that terminates TLS — for example nginx, Caddy, or a cloud load balancer. Never expose the plain-HTTP port directly to the internet. +- [Caddy](#caddy) — simplest; automatic HTTPS, minimal config +- [nginx](#nginx) — most common; full control over headers and limits +- [Traefik](#traefik) — best for Docker; auto-discovers containers -Caddy example (automatic HTTPS): +--- + +## Caddy + +Caddy obtains and renews TLS certificates automatically via Let's Encrypt. ``` stack.example.com { - reverse_proxy localhost:3000 + reverse_proxy localhost:3000 } ``` -## CORS +**Rate limiting** is not built into the standard Caddy binary. Options: -Set `CORS_ORIGINS` to a comma-separated list of the origins that need cross-origin access to your stack: +- Build Caddy with the [`caddy-ratelimit`](https://github.com/mholt/caddy-ratelimit) community module via [caddyserver.com/download](https://caddyserver.com/download). +- Use a CDN or WAF (e.g. Cloudflare) in front of Caddy for IP-based limiting. -``` -CORS_ORIGINS=https://app.example.com,https://admin.example.com +--- + +## nginx + +Obtain a certificate first with [Certbot](https://certbot.eff.org/), then use this configuration. The `limit_req_zone` directives must live in the `http` block (typically `/etc/nginx/nginx.conf` or a file included from it); the `server` block below goes in `/etc/nginx/sites-available/haverstack`. + +```nginx +# In the http block: +limit_req_zone $binary_remote_addr zone=hs_global:10m rate=60r/m; +limit_req_zone $binary_remote_addr zone=hs_tokens:10m rate=5r/m; ``` -The default is empty (no cross-origin access allowed). Set it to `*` only for fully-public, read-only stacks where unauthenticated access is intentional. +```nginx +# /etc/nginx/sites-available/haverstack +server { + listen 80; + server_name stack.example.com; + return 301 https://$host$request_uri; +} -Because the stack is designed to be accessed by many different kinds of apps, you may legitimately need a broad allowlist. The key risk to avoid is combining a wildcard origin with endpoints that accept bearer tokens — browsers will not send `Authorization` headers with credentialed cross-origin requests unless the origin is explicitly listed, so a wildcard does not grant unintended authenticated access from arbitrary origins. +server { + listen 443 ssl; + server_name stack.example.com; + + ssl_certificate /etc/letsencrypt/live/stack.example.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/stack.example.com/privkey.pem; + + location / { + limit_req zone=hs_global burst=20 nodelay; + + proxy_pass http://localhost:3000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # Tighter limit on token issuance to slow brute-force attempts. + location = /tokens { + limit_req zone=hs_tokens burst=2 nodelay; + + proxy_pass http://localhost:3000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} +``` -## Rate limiting +--- + +## Traefik + +Traefik is a good fit when you're already running Docker. It discovers the server container automatically via labels and handles Let's Encrypt itself. + +Create a `docker-compose.yml`: + +```yaml +services: + traefik: + image: traefik:v3 + command: + - --providers.docker=true + - --providers.docker.exposedbydefault=false + - --entrypoints.web.address=:80 + - --entrypoints.web.http.redirections.entrypoint.to=websecure + - --entrypoints.web.http.redirections.entrypoint.scheme=https + - --entrypoints.websecure.address=:443 + - --certificatesresolvers.letsencrypt.acme.email=you@example.com + - --certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json + - --certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web + ports: + - "80:80" + - "443:443" + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + - letsencrypt:/letsencrypt + + server: + image: ghcr.io/haverstack/server:latest + environment: + OWNER_TOKEN: ${OWNER_TOKEN} + ENTITY_ID: ${ENTITY_ID} # only needed on first run + volumes: + - data:/app/data + labels: + - traefik.enable=true + - traefik.http.routers.haverstack.rule=Host(`stack.example.com`) + - traefik.http.routers.haverstack.entrypoints=websecure + - traefik.http.routers.haverstack.tls.certresolver=letsencrypt + # Rate limit: 60 req/min per IP, burst of 20 + - traefik.http.middlewares.haverstack-rl.ratelimit.average=60 + - traefik.http.middlewares.haverstack-rl.ratelimit.period=1m + - traefik.http.middlewares.haverstack-rl.ratelimit.burst=20 + - traefik.http.routers.haverstack.middlewares=haverstack-rl + +volumes: + data: + letsencrypt: +``` -The server does not implement per-IP rate limiting internally. Add it at the reverse-proxy layer. At minimum: +Start with: -- A global per-IP request rate to prevent abuse of public endpoints. -- A stricter rate on `POST /tokens` and authentication-adjacent paths to prevent token brute-forcing. +```sh +docker compose up -d +``` -nginx example: +--- -```nginx -limit_req_zone $binary_remote_addr zone=global:10m rate=60r/m; -limit_req_zone $binary_remote_addr zone=tokens:10m rate=5r/m; +## CORS -server { - location / { - limit_req zone=global burst=20 nodelay; - proxy_pass http://localhost:3000; - } - location /tokens { - limit_req zone=tokens burst=2 nodelay; - proxy_pass http://localhost:3000; - } -} +Set `CORS_ORIGINS` to a comma-separated list of the origins that need cross-origin access to your stack: + +``` +CORS_ORIGINS=https://app.example.com,https://admin.example.com ``` +The default is empty (no cross-origin access allowed). Set it to `*` only for fully-public, read-only stacks where unauthenticated access is intentional. + +Because the stack is designed to be accessed by many different kinds of apps, you may legitimately need a broad allowlist. The key risk to avoid is combining a wildcard origin with endpoints that accept bearer tokens — browsers will not send `Authorization` headers with credentialed cross-origin requests unless the origin is explicitly listed, so a wildcard does not grant unintended authenticated access from arbitrary origins. + ## Public endpoints `GET /.well-known/stack` is intentionally public and unauthenticated. It exposes the owner entity ID, configured timezone, and capability list. This information is required by `@haverstack/adapter-api` to bootstrap a client connection. If your stack is private, ensure the endpoint is only reachable by intended clients (e.g. by network policy) rather than by auth-gating it. From 41b33e97a3e4dc2a2111331caa1e4043a948c076 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 21 Jun 2026 14:44:15 +0000 Subject: [PATCH 4/4] chore: fix prettier formatting --- .github/workflows/release.yml | 6 +++--- CONTRIBUTING.md | 12 ++++++------ README.md | 20 ++++++++++---------- docs/deployment.md | 6 +++--- 4 files changed, 22 insertions(+), 22 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6109b91..2fd8608 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,13 +11,13 @@ jobs: if: "!startsWith(github.event.head_commit.message, 'chore: release')" runs-on: ubuntu-latest permissions: - contents: write # push version bump + tag, create release - packages: write # push Docker image to ghcr.io + contents: write # push version bump + tag, create release + packages: write # push Docker image to ghcr.io steps: - uses: actions/checkout@v4 with: - fetch-depth: 0 # full history so we can inspect commits since last tag + fetch-depth: 0 # full history so we can inspect commits since last tag - uses: pnpm/action-setup@v4 with: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b02e5ca..5060f35 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -24,11 +24,11 @@ All four run in CI and failures block merge. This project uses [Conventional Commits](https://www.conventionalcommits.org/). Every commit message must start with a type prefix. The release workflow reads commit messages since the last tag to determine the version bump automatically: -| Commit type | Example | Version bump | -| --- | --- | --- | -| `feat:` | `feat: add token expiry` | minor | -| `feat!:` or `BREAKING CHANGE` footer | `feat!: rename /entity to /owner` | major | -| Anything else (`fix:`, `chore:`, `docs:`, `refactor:`, `test:`, `perf:`) | `fix: return 404 on missing record` | patch | +| Commit type | Example | Version bump | +| ------------------------------------------------------------------------ | ----------------------------------- | ------------ | +| `feat:` | `feat: add token expiry` | minor | +| `feat!:` or `BREAKING CHANGE` footer | `feat!: rename /entity to /owner` | major | +| Anything else (`fix:`, `chore:`, `docs:`, `refactor:`, `test:`, `perf:`) | `fix: return 404 on missing record` | patch | A scope is optional: `fix(attachments): reject empty filename`. @@ -37,6 +37,6 @@ Getting the type wrong produces the wrong version bump with no warning, so when ## Pull requests - One concern per PR. Split unrelated changes. -- The PR description should explain *why*, not just *what* — the diff already shows what changed. +- The PR description should explain _why_, not just _what_ — the diff already shows what changed. - Link to any relevant issue with `Closes #N`. - Keep PRs small enough to review in one sitting; large refactors are fine but flag them early. diff --git a/README.md b/README.md index 7e515f5..87de424 100644 --- a/README.md +++ b/README.md @@ -40,16 +40,16 @@ docker run -d \ All configuration is via environment variables. See `.env.example` for the full list. -| Variable | Required | Default | Description | -| ---------------------- | --------- | ------------------ | -------------------------------------------------------- | -| `OWNER_TOKEN` | Yes | — | Bearer token for the stack owner. Treat like a password. | -| `ENTITY_ID` | First run | — | Owner entity ID. Only needed when initializing a new DB. | -| `DB_PATH` | No | `/app/data/stack.db` (Docker) / `./stack.db` | Path to the SQLite database file. | -| `PORT` | No | `3000` | Port to listen on. | -| `TIMEZONE` | No | `UTC` | IANA timezone. Only used on first run. | -| `CORS_ORIGINS` | No | `` (none) | Allowed origins, comma-separated or `*`. | -| `BASE_URL` | No | auto-detected | Canonical base URL of this server. | -| `MAX_ATTACHMENT_BYTES` | No | `52428800` (50 MB) | Maximum attachment upload size. | +| Variable | Required | Default | Description | +| ---------------------- | --------- | -------------------------------------------- | -------------------------------------------------------- | +| `OWNER_TOKEN` | Yes | — | Bearer token for the stack owner. Treat like a password. | +| `ENTITY_ID` | First run | — | Owner entity ID. Only needed when initializing a new DB. | +| `DB_PATH` | No | `/app/data/stack.db` (Docker) / `./stack.db` | Path to the SQLite database file. | +| `PORT` | No | `3000` | Port to listen on. | +| `TIMEZONE` | No | `UTC` | IANA timezone. Only used on first run. | +| `CORS_ORIGINS` | No | `` (none) | Allowed origins, comma-separated or `*`. | +| `BASE_URL` | No | auto-detected | Canonical base URL of this server. | +| `MAX_ATTACHMENT_BYTES` | No | `52428800` (50 MB) | Maximum attachment upload size. | --- diff --git a/docs/deployment.md b/docs/deployment.md index f47f628..a1b717d 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -96,8 +96,8 @@ services: - --certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json - --certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web ports: - - "80:80" - - "443:443" + - '80:80' + - '443:443' volumes: - /var/run/docker.sock:/var/run/docker.sock:ro - letsencrypt:/letsencrypt @@ -106,7 +106,7 @@ services: image: ghcr.io/haverstack/server:latest environment: OWNER_TOKEN: ${OWNER_TOKEN} - ENTITY_ID: ${ENTITY_ID} # only needed on first run + ENTITY_ID: ${ENTITY_ID} # only needed on first run volumes: - data:/app/data labels: