A self-hosted, single-merchant electronic withdrawal form implementing the
§ 356a BGB statutory right of withdrawal (mandatory from 2026-06-19).
Neutral by default, configurable per .env, open-source (AGPL-3.0).
- Docker (Engine 25+) and Docker Compose (v2.24+)
- Task (go-task) — optional, but used for all convenience commands
# First run — build image, install deps, build assets, generate key, migrate
task init
# Start the dev stack (http://localhost:8580)
task up
# Run the quality gate (Pint + PHPStan + Pest)
task check
# Start the Vite dev server (HMR) — separate terminal
task devThe dev stack uses a source bind-mount so every local edit is reflected immediately.
task up auto-merges docker-compose.override.yml (the dev overlay) with the base
docker-compose.yml.
docker run --rm ghcr.io/chriras/revoco:latest \
php artisan key:generate --showghcr.io/chriras/revoco is the official published image. If you fork and build
your own, substitute your (lowercase) GHCR owner.
Copy the output (base64:...) — this becomes APP_KEY in your environment.
Never let the container generate its own key on boot; see the entrypoint.
cp .env.example .env
# Edit .env: set APP_KEY, APP_URL, MAIL_*, OPERATOR_EMAIL, OPERATOR_PASSWORD, …docker compose -f docker-compose.yml up -dThe app container:
- Fails fast if
APP_KEYis missing (exits non-zero — never auto-generates). - Creates storage directories and the SQLite file on the persistent volume.
- Runs
migrate --force. - Warms config/route/view caches.
- Starts
php-fpm.
nginx (web) serves static files from the app_public shared volume and
proxies PHP requests to app:9000.
docker compose -f docker-compose.yml exec app \
php artisan app:operator --email=admin@example.com --password=changemeOr set OPERATOR_EMAIL / OPERATOR_PASSWORD in .env and run task operator.
# Start with the bundled ntfy service
docker compose -f docker-compose.yml --profile ntfy up -dConfigure NTFY_ENABLED=true, NTFY_SERVER, NTFY_TOPIC (and optionally
NTFY_TOKEN) in .env.
Build the prod image and run a local end-to-end smoke test (HTTP 200 on /):
task smokeRuns locally and in the release CI workflow.
Deployment configuration is via environment variables. See .env.example
for the full list with inline documentation. Content-level settings — the legal pages
(imprint & privacy), consumer languages, withdrawal scope, and the withdrawal-notification
recipient — are managed by the operator in the Filament panel and persist in the database,
so changing them needs no redeploy. Key environment variables:
| Variable | Required | Default | Notes |
|---|---|---|---|
APP_KEY |
Yes | — | Generate with key:generate --show. Fail-fast if missing. |
APP_URL |
Yes | http://localhost |
Full public URL incl. scheme. |
APP_TIMEZONE |
No | UTC |
Consumer-local time for withdrawal timestamps. |
APP_THEME |
No | neutral |
Visual theme token set (data-theme). |
DB_DATABASE |
No | /var/www/html/storage/database/database.sqlite |
SQLite path inside container. |
QUEUE_CONNECTION |
No | database |
Use database for the bundled SQLite queue. |
MAIL_MAILER |
No | log (safe) |
Set to smtp in prod and configure MAIL_*. |
MERCHANT_NOTIFICATION_EMAIL |
No | — | Fallback recipient for the operator notification. The in-panel setting takes precedence; if neither is set, the imprint e-mail is used. |
NTFY_ENABLED |
No | false |
Opt-in ntfy push (no PII sent). |
The form is neutral by default. Its whole appearance is driven by one --wf-* CSS
token contract, switched via data-theme (APP_THEME). Concrete brand overlays —
real colours, fonts and logos — never ship in this public repo; they live in your
private infra repo / deployment mount (see
.claude/project/design/theming.md).
To match a shop's look without hand-writing CSS, this repo ships a deploy-time
Claude-Code skill, design-adoption (in
.claude/skills/design-adoption/) — an
authoring tool, not an app-runtime feature. Run it inside a Claude Code session:
give it the shop's URL and it scans the site, extracts the corporate identity
(colours, fonts, logo), and produces a ready-to-place brand overlay plus a
placement report.
Under the hood it calls a deterministic Artisan command you can also run directly:
task artisan -- revoco:make-theme \
--slug=myshop \
--accent='#4f46e5' --fg='#1f2937' \
--font='"Inter", system-ui, sans-serif' \
--logo-url='https://myshop.example/logo.svg' \
--brand-name='My Shop' \
--output=./myshop-theme.cssThe command validates every value against the --wf-* contract, rejects malformed
input, and never overrides accessibility-critical tokens (the focus ring stays
intact). The result is operator-reviewed and operator-placed: move the overlay
into your deployment, set APP_THEME=<slug>, wire the logo via REVOCO_LOGO_URL,
and review the rendered form before going live. Nothing is auto-published, and
brand assets are never committed to this repo.
Revoco serves an Impressum (§ 5 DDG) and a privacy policy at /impressum and
/datenschutz, backed by operator-editable settings in the Filament Legal panel
(neutral "not configured yet" placeholders until filled). The operator is the data
controller and authors the substantive text — Revoco ships only the mechanism.
If the operator already publishes an Impressum and privacy policy elsewhere (their shop,
a CMS), this repo ships a deploy-time Claude-Code skill, legal-extraction (in
.claude/skills/legal-extraction/), to
import them instead of copying field by field. Give it the two URLs and the locale; it
scrapes the pages, extracts the § 5 DDG fields and the privacy content, and hands a JSON
payload to a deterministic Artisan command:
task artisan -- revoco:import-legal --locale=de --input=./legal-payload.jsonThe command validates the payload against the settings schema (unknown key or malformed
e-mail → nothing written), sanitises scraped HTML, scopes per-locale fields to --locale,
and refuses to overwrite already-populated fields unless --overwrite is given — your
reviewed legal text is never silently replaced. The import is operator-reviewed: open
Filament → Legal, correct every field, and check the rendered pages before relying on them.
Revoco makes no legal-correctness guarantee.
GitHub Actions are configured in .github/workflows/:
ci.yml— runscomposer audit(security) +Pint --test+ PHPStan (max)- Pest on every PR and push to
main.
- Pest on every PR and push to
release.yml— on av*SemVer tag: builds the prod image, runs the smoke test, and pushes toghcr.io/chriras/revoco(SemVer +latest). A fork publishes under its own lowercased owner.security-audit.yml— scheduledcomposer audit(~every 2 days) that opens an issue if a locked dependency has a known advisory. Dependabot (.github/dependabot.yml) complements it with weekly update PRs.
AGPL-3.0-or-later. See LICENSE.