Run locally like the sibling apps: fixed ports, the pod provider in Docker, the app outside

Festipod now follows the local convention of welcometomyplace, mutual-aid.app
and portejunes: one fixed port pair per app (Festipod defaults: backend 3005,
app 4005; pod provider 3000/5000), overridable in backend/.env and .env.
`make start` runs docker-compose-dev.yml (the local pod provider from images,
Fuseki, Redis, mailcatcher, Arena); the backend and the app run outside Docker
with `cd backend && pnpm dev` and `pnpm dev`. pnpm stays the package manager.

The old docker-compose.yml, which containerized Festipod's own frontend and
backend and could not reach the host's Fuseki and Redis, is gone; portless is
dropped from the dev script, since the backend declares a single OIDC redirect
URI. The local backend runs without a Redis cache, as the sibling apps do. The
e2e suites and validate read the app, backend and pod provider URLs from the
env files instead of hardcoding them.

The deployment and local cookbook leaves record the traps met while resetting
datasets: a dataset re-created under the same name needs a Fuseki restart
(ClosedException otherwise), the anonymous WebACL probe at boot is expected,
and a change of the backend's URL leaves persistent accounts with a
participations collection the new app identity cannot control.
This commit is contained in:
Sylvain Duchesne
2026-11-01 11:40:27 +01:00
parent 908f7ea973
commit 5e3e821e2a
33 changed files with 280 additions and 129 deletions
+2 -2
View File
@@ -20,10 +20,10 @@ import { fetchCollectionMemberIds } from '../src/shared/data/contacts';
const TS = Date.now();
const PASS = 'Test1test';
const POD = 'http://localhost:3000';
const POD = process.env.VITE_POD_PROVIDER_URL || 'http://localhost:3000';
const A_NAME = `diaginv${TS}a`;
const B_NAME = `diaginv${TS}b`;
const config = { podProviderUrl: POD, backendUrl: process.env.BACKEND_URL || 'http://localhost:3003' };
const config = { podProviderUrl: POD, backendUrl: process.env.BACKEND_URL || process.env.VITE_BACKEND_URL || 'http://localhost:3005' };
const CRED_CTX = [
'https://www.w3.org/ns/credentials/v2',
+5 -5
View File
@@ -9,7 +9,7 @@
* runs it end-to-end and fails on any UNEXPECTED ≥400, so the regression is
* caught without a human driving the browser.
*
* ORIGIN. The OIDC redirect_uri is registered for `http://localhost:4000/login`
* ORIGIN. The OIDC redirect_uri is registered for the app origin `/login` (default http://localhost:4005)
* only (the app's FRONT_URL), so the app MUST be served at APP_URL — do not
* serve a private instance on an ephemeral port: the pod rejects the redirect.
*
@@ -18,7 +18,7 @@
* leave the app for the pod, the consent Accept is clicked once stable and the
* page must then leave the consent screen, and the shell counts only with the
* bottom navigation shown and no picker). The previous local loop could report
* "shell reached" while still on `:4000/` — the user never consented, the app
* "shell reached" while still on the app origin `/` — the user never consented, the app
* was never installed. `authenticated` also requires the install to be visible
* on the pod (the user's registered Event container answers 200).
*
@@ -31,10 +31,10 @@ import type { ApodsStackConfig } from '../src/shared/support/activitypodsStack';
import { driveLogin, installedContainer, loginWithRetry, type DriveContext } from './e2e-rights-matrix-support';
const strip = (u: string): string => u.replace(/\/+$/, '');
const BACKEND: string = strip(process.env.BACKEND_URL || 'http://localhost:3003');
const APP: string = strip(process.env.APP_URL || 'http://localhost:4000');
const BACKEND: string = strip(process.env.BACKEND_URL || process.env.VITE_BACKEND_URL || 'http://localhost:3005');
const APP: string = strip(process.env.APP_URL || `http://localhost:${process.env.PORT || 4005}`);
const config: ApodsStackConfig = {
podProviderUrl: strip(process.env.POD_PROVIDER_URL || 'http://localhost:3000'),
podProviderUrl: strip(process.env.POD_PROVIDER_URL || process.env.VITE_POD_PROVIDER_URL || 'http://localhost:3000'),
backendUrl: BACKEND,
};
const POD_FRONT: string = strip(process.env.POD_FRONTEND_URL || 'http://localhost:5000');
+4 -4
View File
@@ -35,7 +35,7 @@
* visibilitychange); its one-minute throttle is untouched.
*
* ORIGIN. As for `e2e-login-flow.ts`: the OIDC redirect_uri is registered for
* `http://localhost:4000/login` only, so the app MUST be served at APP_URL.
* the app origin `/login` (default http://localhost:4005) only, so the app MUST be served at APP_URL.
*
* Exit code 0 only when every phase holds; the JSON report says which did not.
*/
@@ -52,10 +52,10 @@ import {
import { driveLogin, type DriveContext } from './e2e-rights-matrix-support';
const strip = (u: string): string => u.replace(/\/+$/, '');
const BACKEND: string = strip(process.env.BACKEND_URL || 'http://localhost:3003');
const APP: string = strip(process.env.APP_URL || 'http://localhost:4000');
const BACKEND: string = strip(process.env.BACKEND_URL || process.env.VITE_BACKEND_URL || 'http://localhost:3005');
const APP: string = strip(process.env.APP_URL || `http://localhost:${process.env.PORT || 4005}`);
const config: ApodsStackConfig = {
podProviderUrl: strip(process.env.POD_PROVIDER_URL || 'http://localhost:3000'),
podProviderUrl: strip(process.env.POD_PROVIDER_URL || process.env.VITE_POD_PROVIDER_URL || 'http://localhost:3000'),
backendUrl: BACKEND,
};
const POD_FRONT = strip(process.env.POD_FRONTEND_URL || 'http://localhost:5000');
+1 -1
View File
@@ -379,7 +379,7 @@ export interface LoginOutcome { ok: boolean; detail: string }
* still open (a sign-out of the app keeps it: the revocation probe's second
* sign-in), in which case no form is shown and the hops below start at once;
* after the credentials the
* browser comes BACK to the app's OIDC callback (:4000/login?code…), and only
* browser comes BACK to the app's OIDC callback (<app origin>/login?code…), and only
* then — when the app is not yet registered on this pod — does the app send it
* to the pod's consent screen (:5000/authorize), which navigates a second time
* right after it opens. Accept leads back to the app, then to the shell.
+4 -4
View File
@@ -109,7 +109,7 @@
* the forged state itself could not be produced.
*
* ORIGIN. As for `e2e-login-flow.ts`: the OIDC redirect_uri is registered for
* `http://localhost:4000/login` only, so the app MUST be served at APP_URL.
* the app origin `/login` (default http://localhost:4005) only, so the app MUST be served at APP_URL.
*/
import { writeFileSync } from 'node:fs';
@@ -158,11 +158,11 @@ import {
const strip = (u: string): string => u.replace(/\/+$/, '');
const BACKEND = strip(process.env.BACKEND_URL || 'http://localhost:3003');
const APP = strip(process.env.APP_URL || 'http://localhost:4000');
const BACKEND = strip(process.env.BACKEND_URL || process.env.VITE_BACKEND_URL || 'http://localhost:3005');
const APP = strip(process.env.APP_URL || `http://localhost:${process.env.PORT || 4005}`);
const POD_FRONT = strip(process.env.POD_FRONTEND_URL || 'http://localhost:5000');
const config: ApodsStackConfig = {
podProviderUrl: strip(process.env.POD_PROVIDER_URL || 'http://localhost:3000'),
podProviderUrl: strip(process.env.POD_PROVIDER_URL || process.env.VITE_POD_PROVIDER_URL || 'http://localhost:3000'),
backendUrl: BACKEND,
};
+1 -1
View File
@@ -37,7 +37,7 @@
*
* USAGE (reactive dev):
* Terminal 1: pnpm run overlay:polyfill # or: pnpm run overlay:indexing
* Terminal 2: bun run dev # portless festipod bun --hot src/index.ts
* Terminal 2: pnpm dev # bun --hot src/index.ts
* Edit the checkout's src → it lands in node_modules → RESTART dev to pick it up.
*
* pnpm run overlay:indexing --once # overlay + verify, no watch (CI / one-shot)
+3 -3
View File
@@ -76,10 +76,10 @@ const TIMEOUT_REVOCATION_MS = 15 * 60 * 1000; // 15 min (the probe bounds itself
/** The local stack, with the same defaults and env overrides as the e2e scripts. */
const strip = (u: string): string => u.replace(/\/+$/, "");
const POD_PROVIDER = strip(process.env.POD_PROVIDER_URL || "http://localhost:3000");
const POD_PROVIDER = strip(process.env.POD_PROVIDER_URL || process.env.VITE_POD_PROVIDER_URL || 'http://localhost:3000');
const POD_FRONT = strip(process.env.POD_FRONTEND_URL || "http://localhost:5000");
const BACKEND = strip(process.env.BACKEND_URL || "http://localhost:3003");
const APP = strip(process.env.APP_URL || "http://localhost:4000");
const BACKEND = strip(process.env.BACKEND_URL || process.env.VITE_BACKEND_URL || 'http://localhost:3005');
const APP = strip(process.env.APP_URL || `http://localhost:${process.env.PORT || 4005}`);
const FUSEKI = strip(process.env.FUSEKI_URL || "http://localhost:3030");
/** What the browser-driven e2e probes need answering 200 — every piece of the local stack. */