diff --git a/README.md b/README.md index 5256676..493934f 100644 --- a/README.md +++ b/README.md @@ -30,20 +30,6 @@ Node >= 22.12. | ---------------- | -------------------------------------------- | ------------------------------------ | | `POCKETBASE_URL` | URL of the PocketBase backend (auth + data) | `https://pokeshare.namarusaja.me` | -PocketBase container (compose only): - -| Variable | Description | -| ------------------------------------ | ------------------------------------------------- | -| `PB_SUPERUSER_EMAIL` / `_PASSWORD` | Create/update the admin account on boot | -| `PB_APP_URL` | Public app URL used in transactional email links | -| `SMTP_HOST` / `SMTP_PORT` | Mail server (mail disabled if `SMTP_HOST` empty) | -| `SMTP_USERNAME` / `SMTP_PASSWORD` | Mail server credentials | -| `SMTP_TLS` | `true` (default) / `false` | -| `SMTP_SENDER_NAME` / `_ADDRESS` | From header of transactional emails | - -SMTP settings are applied on every boot by `pocketbase/pb_hooks/main.pb.js` -from these env vars, so they can be changed without touching the admin UI. - Read at **runtime** (`process.env`), so the same Docker image can be deployed against different PocketBase instances. For local dev, copy `.env.example` to `.env`. `HOST` and `PORT` are also honored by the standalone server. @@ -59,24 +45,18 @@ docker run -p 4321:4321 -e POCKETBASE_URL=https://pb.example.com pokeshare ## Full stack (app + PocketBase) -`docker-compose.yml` runs the app together with a PocketBase container. On -first boot, PocketBase applies `pocketbase/pb_migrations/`, which creates the -`users` profile fields, the `public_users` view, and the `mycards` and -`friendships` collections with their API rules — no manual setup needed. +`docker-compose.yml` runs just the app; it expects an external PocketBase +instance referenced by `POCKETBASE_URL` (see above). A schema export of the +expected collections is kept at `pb_schema.json` for reference when setting +up a new PocketBase instance (admin UI → Settings → Import collections). ```sh -PB_SUPERUSER_EMAIL=admin@example.com PB_SUPERUSER_PASSWORD=changeme123 \ - docker compose up --build +POCKETBASE_URL=https://pb.example.com docker compose up --build ``` -- The app listens on `4321`; PocketBase listens on `8090` (internal network, - admin UI at `/_/` if you expose it). -- In Coolify, deploy as a **Docker Compose** resource and expose the `app` - service on your domain. Set `PB_SUPERUSER_*` in the environment variables. -- Accounts are created from the PocketBase admin UI (public registration is - disabled by the migration). -- To move existing data, use the PocketBase admin UI backups (Settings → - Backups) on the old instance and restore into `pb_data`. +- The app listens on `4321`; expose it on your domain (Coolify: expose the + `app` service, or use the plain Dockerfile build pack). +- Accounts are managed from the PocketBase admin UI (`/_/`). ## Structure diff --git a/docker-compose.yml b/docker-compose.yml index 4547532..8f48892 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,46 +1,13 @@ -# Full PokeShare stack: the Astro app and its PocketBase backend. -# In Coolify, deploy this file as a "Docker Compose" resource. -# The `app` service is the one to expose on your public domain. -# `pocketbase` stays on the internal network — expose it on a subdomain -# only if you want to reach the admin UI (/_/) from outside. +# PokeShare app, pointing at an external PocketBase instance. +# In Coolify, deploy this file as a "Docker Compose" resource and expose the +# `app` service on your public domain. services: - pocketbase: - build: ./pocketbase - restart: unless-stopped - environment: - # Optional: create/update the admin account on boot (min 8 chars) - PB_SUPERUSER_EMAIL: ${PB_SUPERUSER_EMAIL:-} - PB_SUPERUSER_PASSWORD: ${PB_SUPERUSER_PASSWORD:-} - # Optional: app URL used in transactional emails (reset links...) - PB_APP_URL: ${PB_APP_URL:-} - # Optional: SMTP for transactional emails. If SMTP_HOST is empty, - # mail stays disabled (the rest is ignored). - SMTP_HOST: ${SMTP_HOST:-} - SMTP_PORT: ${SMTP_PORT:-587} - SMTP_USERNAME: ${SMTP_USERNAME:-} - SMTP_PASSWORD: ${SMTP_PASSWORD:-} - SMTP_TLS: ${SMTP_TLS:-true} - SMTP_SENDER_NAME: ${SMTP_SENDER_NAME:-PokeShare} - SMTP_SENDER_ADDRESS: ${SMTP_SENDER_ADDRESS:-} - volumes: - - pb_data:/pb/pb_data - healthcheck: - test: "curl -fsS http://localhost:8090/api/health || exit 1" - interval: 30s - timeout: 5s - retries: 5 - start_period: 15s - app: build: . restart: unless-stopped environment: - # Internal service URL — the browser never talks to PocketBase - # directly, everything goes through the Astro server. - POCKETBASE_URL: http://pocketbase:8090 - depends_on: - pocketbase: - condition: service_healthy + # Full URL of the PocketBase instance to use (required). + POCKETBASE_URL: ${POCKETBASE_URL:-https://pokeshare.namarusaja.me} healthcheck: # / redirects to /login (302) — check a 200 endpoint instead test: "wget -q -O /dev/null http://localhost:4321/login || exit 1" @@ -48,6 +15,3 @@ services: timeout: 5s retries: 5 start_period: 15s - -volumes: - pb_data: diff --git a/pocketbase/pb_schema.json b/pb_schema.json similarity index 100% rename from pocketbase/pb_schema.json rename to pb_schema.json diff --git a/pocketbase/Dockerfile b/pocketbase/Dockerfile deleted file mode 100644 index 9c1f10a..0000000 --- a/pocketbase/Dockerfile +++ /dev/null @@ -1,20 +0,0 @@ -# PocketBase backend with auto-applied migrations (collections + rules). -# Migrations in ./pb_migrations run automatically when the server starts. -FROM alpine:3.21 - -ARG PB_VERSION=0.39.9 - -RUN apk add --no-cache ca-certificates curl unzip - -WORKDIR /pb -RUN curl -fsSL "https://github.com/pocketbase/pocketbase/releases/download/v${PB_VERSION}/pocketbase_${PB_VERSION}_linux_amd64.zip" -o pb.zip \ - && unzip pb.zip \ - && rm pb.zip - -COPY pb_migrations ./pb_migrations -COPY pb_hooks ./pb_hooks -COPY entrypoint.sh ./ -RUN chmod +x entrypoint.sh pocketbase - -EXPOSE 8090 -CMD ["./entrypoint.sh"] diff --git a/pocketbase/entrypoint.sh b/pocketbase/entrypoint.sh deleted file mode 100644 index c0eb76b..0000000 --- a/pocketbase/entrypoint.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/sh -set -e - -# Optionally create/update the admin (superuser) account on boot. -# Set PB_SUPERUSER_EMAIL and PB_SUPERUSER_PASSWORD as env vars to enable. -# The password must be at least 8 characters. -if [ -n "$PB_SUPERUSER_EMAIL" ] && [ -n "$PB_SUPERUSER_PASSWORD" ]; then - ./pocketbase superuser upsert "$PB_SUPERUSER_EMAIL" "$PB_SUPERUSER_PASSWORD" || true -fi - -# `serve` automatically applies pending migrations from ./pb_migrations -exec ./pocketbase serve --http=0.0.0.0:8090 diff --git a/pocketbase/pb_hooks/main.pb.js b/pocketbase/pb_hooks/main.pb.js deleted file mode 100644 index a44df34..0000000 --- a/pocketbase/pb_hooks/main.pb.js +++ /dev/null @@ -1,36 +0,0 @@ -/** - * Apply app identity and SMTP settings from environment variables on every - * boot, so mail config stays declarative (docker-compose / Coolify env vars). - * These values win over the admin UI (Settings → Mail) — change them via env. - */ -onBootstrap((e) => { - e.next(); - - const settings = $app.settings(); - - // App identity, used in transactional emails (verification, reset, etc.) - settings.meta.appName = "PokeShare"; - const appUrl = $os.getenv("PB_APP_URL"); - if (appUrl) settings.meta.appUrl = appUrl; - - const host = $os.getenv("SMTP_HOST"); - if (!host) { - // No SMTP configured — save identity settings and keep mail disabled - $app.save(settings); - return; - } - - const senderAddress = $os.getenv("SMTP_SENDER_ADDRESS"); - const senderName = $os.getenv("SMTP_SENDER_NAME"); - if (senderAddress) settings.meta.senderAddress = senderAddress; - if (senderName) settings.meta.senderName = senderName; - - settings.smtp.enabled = true; - settings.smtp.host = host; - settings.smtp.port = parseInt($os.getenv("SMTP_PORT") || "587", 10); - settings.smtp.username = $os.getenv("SMTP_USERNAME") || ""; - settings.smtp.password = $os.getenv("SMTP_PASSWORD") || ""; - settings.smtp.tls = ($os.getenv("SMTP_TLS") || "true") === "true"; - - $app.save(settings); -}); diff --git a/pocketbase/pb_migrations/1700000000_init.js b/pocketbase/pb_migrations/1700000000_init.js deleted file mode 100644 index a1757ca..0000000 --- a/pocketbase/pb_migrations/1700000000_init.js +++ /dev/null @@ -1,121 +0,0 @@ -/** - * Initial PokeShare schema: extends the default `users` auth collection and - * creates `public_users`, `mycards` and `friendships` with their API rules. - * Mirrors the production schema (pb_schema.json export). - * Applied automatically by `pocketbase serve` on first boot. - */ -migrate((app) => { - // --- users (auth collection): extra profile fields, self-only access --- - const users = app.findCollectionByNameOrId("users"); - users.fields.add( - { name: "username", type: "text", required: true, min: 3, max: 20 }, - { - name: "theme_preference", - type: "select", - values: ["System", "Light", "Dark"], - maxSelect: 1, - }, - { - name: "visibility", - type: "select", - values: ["Public", "Friends", "Private"], - maxSelect: 1, - }, - ); - users.indexes.push( - "CREATE UNIQUE INDEX idx_users_username ON users (username COLLATE NOCASE)", - ); - users.listRule = "id = @request.auth.id"; - users.viewRule = "id = @request.auth.id"; - users.updateRule = "id = @request.auth.id"; - // No public registration — accounts are created from the admin UI (/_/) - users.createRule = null; - app.save(users); - - // --- public_users: read-only view used by the friend search --- - const publicUsers = new Collection({ - name: "public_users", - type: "view", - viewQuery: `SELECT id, username, name, visibility FROM users WHERE visibility != 'Private'`, - listRule: '@request.auth.id != ""', - viewRule: null, - }); - app.save(publicUsers); - - // --- mycards: one row per owned card --- - // listRule enforces collection visibility at the database level: - // owner, or anyone if the owner is Public, or accepted friends if Friends. - const mycards = new Collection({ - name: "mycards", - type: "base", - fields: [ - { name: "card_id", type: "text" }, - { name: "card_name", type: "text" }, - { - name: "user", - type: "relation", - collectionId: "_pb_users_auth_", - maxSelect: 1, - cascadeDelete: true, - }, - { name: "card_image", type: "url" }, - { name: "variant", type: "text" }, - { name: "quantity", type: "number" }, - { name: "set_name", type: "text" }, - { name: "notes", type: "text" }, - { name: "condition", type: "text" }, - ], - listRule: - "@request.auth.id = user || (user.visibility = 'Public') || (user.visibility = 'Friends' && ((@collection.friendships.requester = user && @collection.friendships.addressee = @request.auth.id && @collection.friendships.status = 'accepted') || (@collection.friendships.addressee = user && @collection.friendships.requester = @request.auth.id && @collection.friendships.status = 'accepted')))", - viewRule: - "@request.auth.id = user || (@collection.friendships.requester = user && @collection.friendships.addressee = @request.auth.id && @collection.friendships.status = 'accepted') || (@collection.friendships.addressee = user && @collection.friendships.requester = @request.auth.id && @collection.friendships.status = 'accepted')", - createRule: '@request.auth.id != "" && user = @request.auth.id', - updateRule: '@request.auth.id != "" && user = @request.auth.id', - deleteRule: '@request.auth.id != "" && user = @request.auth.id', - }); - app.save(mycards); - - // --- friendships --- - const friendships = new Collection({ - name: "friendships", - type: "base", - fields: [ - { - name: "requester", - type: "relation", - collectionId: "_pb_users_auth_", - maxSelect: 1, - cascadeDelete: true, - }, - { - name: "addressee", - type: "relation", - collectionId: "_pb_users_auth_", - maxSelect: 1, - cascadeDelete: true, - }, - { - name: "status", - type: "select", - values: ["accepted", "pending", "declined"], - maxSelect: 1, - }, - ], - listRule: "@request.auth.id = requester || @request.auth.id = addressee", - viewRule: "@request.auth.id = requester || @request.auth.id = addressee", - createRule: "@request.auth.id = requester && addressee != requester", - updateRule: "@request.auth.id = addressee || @request.auth.id = requester", - deleteRule: "@request.auth.id = addressee || @request.auth.id = requester", - }); - app.save(friendships); -}, (app) => { - // Down: drop the custom collections. The added `users` fields are left in - // place — removing them would destroy data and they are harmless. - for (const name of ["friendships", "mycards", "public_users"]) { - try { - app.delete(app.findCollectionByNameOrId(name)); - } catch { - // already missing - } - } -});