|
|
|
@@ -12,11 +12,13 @@ last_checked: 2026-10-19
|
|
|
|
|
|
|
|
|
|
**One base URL per environment** (user decision 2026-10-19, so a pull-request preview lives on a single host, like the sibling apps): the app at `https://festipod.dev.reconnexion.coop/`, the backend on the same host under `/api` — its Domains field is `https://<host>/api,https://<host>/.well-known`. Coolify's **Strip Prefixes must be OFF** (it is on the sibling apps): the backend mounts its routes under `/api` itself, read from `SEMAPPS_HOME_URL=https://<host>/api/`, and the front end is pointed at `VITE_BACKEND_URL=https://<host>/api`. With the prefix stripped every backend request is refused (404).
|
|
|
|
|
|
|
|
|
|
**The backend is base-path aware** (`backend/services/base-path.js`), because SemApps is: it mounts every route it adds under the base URL's path (LDP resources, the app actor and its inbox, public keys, access needs, webhooks, `/nodeinfo/2.1`) but keeps the `/.well-known` ones (`context.jsonld`, `nodeinfo`, `solid`, `void`) at the origin root. Festipod's own routes (`/app`, `/as/actor/app`, `/catalog`) are mounted under the base path the same way, the public-surface allow-list matches paths relative to it (the two `/.well-known` endpoints against the full path), and the Solid-OIDC `client_id` is the requested URI rebuilt from the base URL as written — so it equals, byte for byte, the actor URI the backend mints and the `${backendUrl}/as/actor/app` the front end derives. At the root (`http://localhost:3003`, local dev) every decision is identical to the pre-base-path code. To verify: `bun test backend/services` (with and without `SEMAPPS_HOME_URL=https://<host>/api/`).
|
|
|
|
|
**The backend is base-path aware** (`backend/services/base-path.js`), because SemApps is: it mounts every route it adds under the base URL's path (LDP resources, the app actor and its inbox, public keys, access needs, webhooks, `/nodeinfo/2.1`) but keeps the `/.well-known` ones (`context.jsonld`, `nodeinfo`, `solid`, `void`) at the origin root. Festipod's own routes (`/app`, `/as/actor/app`, `/catalog`) are mounted under the base path the same way, the public-surface allow-list matches paths relative to it (the two `/.well-known` endpoints against the full path), and the Solid-OIDC `client_id` is the requested URI rebuilt from the base URL as written — so it equals, byte for byte, the actor URI the backend mints and the `${backendUrl}/as/actor/app` the front end derives. At the root (`http://localhost:3005`, local dev) every decision is identical to the pre-base-path code. To verify: `bun test backend/services` (with and without `SEMAPPS_HOME_URL=https://<host>/api/`).
|
|
|
|
|
|
|
|
|
|
**First-boot traps.**
|
|
|
|
|
- The backend registers its app actor in its dataset with the domain it has on the FIRST boot: set the domains in Coolify before the first deployment.
|
|
|
|
|
- The backend runs with WebACL on, as the reference apps do, so its main dataset must be SECURE: the backend creates it so when it does not exist, and refuses to start ("the main dataset is not secure") on an existing one created otherwise. Starting again from nothing (domains changed, or a dataset created before WebACL was on): delete the two datasets in Fuseki AND the secure dataset's files Fuseki's delete leaves on disk (`databases/<main>`, `<main>Acl`, `<main>Mirror`, `configuration/<main>.ttl`), empty the stack's Redis (its cache keeps serving the old app actor, keys and access needs), redeploy — then every user installs the app again. The exact steps are in the compose file's header.
|
|
|
|
|
- The backend runs with WebACL on, as the reference apps do, so its main dataset must be SECURE: the backend creates it so when it does not exist, and refuses to start ("the main dataset is not secure") on an existing one created otherwise. Starting again from nothing (domains changed, or a dataset created before WebACL was on): delete the two datasets in Fuseki AND the secure dataset's files Fuseki's delete leaves on disk (`databases/<main>`, `<main>Acl`, `<main>Mirror`, `configuration/<main>.ttl`), empty the stack's Redis (its cache keeps serving the old app actor, keys and access needs), redeploy — then every user installs the app again. The exact steps are in the compose file's header. Two more traps, both met on the test server 2026-10-30:
|
|
|
|
|
- **A deleted dataset cannot be re-created under the same name in the same Fuseki process**: Jena keeps the closed storage of that location, and every query on the new dataset fails with `ClosedException: already closed` (500), so the backend crashes at boot. Restart Fuseki before re-creating it — on the test server it is the shared one, so every test app loses its triplestore meanwhile — or give the stack new dataset names (`MAIN_DATASET` / `SETTINGS_DATASET`, e.g. `festipod-test-2`).
|
|
|
|
|
- A "Model permissions violation: http://semapps.org/securedWebacl" in Fuseki's log at the backend's boot is EXPECTED: semapps probes the WebACL graph anonymously to check the dataset is secure and wants that refusal. The backend must reach Fuseki as the Fuseki user named exactly `admin` (`JENA_USER`), the only one the semapps permissions module grants a secure dataset to (semapps `ShiroEvaluator`, read in source).
|
|
|
|
|
- On a cold first boot, `GET <base>app` answers from an in-memory copy of the actor taken before the access-need group got linked, so it lacks `interop:hasAccessNeedGroup` until the backend restarts (the actor itself, `<base>as/actor/app`, is right). A real sign-in is unaffected — the pod reads the actor — but the rights matrix's programmatic install reads `/app`: restart the backend once after a first deployment.
|
|
|
|
|
|
|
|
|
|
**Verified 2026-10-19, locally, not yet on Coolify**: both images built from a clean export of the tree, run against the local pod provider behind a same-origin proxy that routes `/api` and `/.well-known` to the backend unstripped with the Host header kept (as Traefik does) — the login flow, the rights matrix (98/98 hard lines) and the revocation probe all pass; the same images at the origin root pass them too.
|
|
|
|
@@ -56,7 +58,7 @@ A `Dockerfile` exists (multi-stage Bun Alpine). **Installation goes through pnpm
|
|
|
|
|
|
|
|
|
|
## Environment variables
|
|
|
|
|
|
|
|
|
|
- `PORT` (default 3000), `NODE_ENV` (enables/disables HMR and the dev auto-seed — see concept `data-layer`).
|
|
|
|
|
- `PORT` (default 4005), `NODE_ENV` (enables/disables HMR and the dev auto-seed — see concept `data-layer`).
|
|
|
|
|
- No `.env*` is committed (`.env` is gitignored).
|
|
|
|
|
|
|
|
|
|
### The shared wallet: config, not a secret, not a mount
|
|
|
|
@@ -73,6 +75,6 @@ A `Dockerfile` exists (multi-stage Bun Alpine). **Installation goes through pnpm
|
|
|
|
|
|
|
|
|
|
## Dev
|
|
|
|
|
|
|
|
|
|
`bun run dev` = **`portless festipod bun --hot src/index.ts`** — it goes through the **`portless`** wrapper (an external port-management tool), not a bare `bun --hot`. HMR is active outside production.
|
|
|
|
|
`pnpm dev` = **`bun --hot src/index.ts`**, served on the fixed port `PORT` (root `.env`, default 4005) like the sibling ActivityPods apps — no port-management wrapper. HMR is active outside production. The local stack (pod provider, backend, ports) is in [[cookbook_local-activitypods-platform]].
|
|
|
|
|
|
|
|
|
|
**Reactive local overlay for the SDK**: in production the `@ng-eventually/polyfill` dependency comes from Gitea (git+https, pinned by `pnpm-lock.yaml`), and `@ng-helpers/indexing` likewise. When a provider's package has to be exercised from a local checkout, `pnpm run overlay:polyfill` or `pnpm run overlay:indexing` (script `scripts/overlay-local-checkout.ts`, one provider per run) replaces `node_modules/<package>` with a **real copy** of that checkout (location overridable with `NG_EVENTUALLY_LOCAL` / `NG_HELPERS_LOCAL`) — **without** its own `node_modules/*` — and resyncs on every edit. Copying rather than symlinking is what keeps a **single instance** of every package the provider shares with Festipod installed (`@ng-org/*`, and for `indexing`, `@ng-eventually/polyfill` itself): a symlink would drag in a second one and the SDK would stop working. To go back to the committed state: `pnpm install`.
|
|
|
|
|