|
|
3 days ago | |
|---|---|---|
| assets | 3 days ago | |
| bin | 5 days ago | |
| config | 4 days ago | |
| deploy | 5 days ago | |
| docker/cron | 5 days ago | |
| docs | 9 months ago | |
| frankenphp | 4 days ago | |
| migrations | 4 days ago | |
| public | 5 days ago | |
| publication/Newsroom | 9 months ago | |
| scripts | 5 days ago | |
| src | 3 days ago | |
| templates | 4 days ago | |
| tests | 11 months ago | |
| translations | 9 months ago | |
| .dockerignore | 6 days ago | |
| .editorconfig | 1 year ago | |
| .env.dist | 5 days ago | |
| .env.test | 1 year ago | |
| .gitattributes | 1 year ago | |
| .gitignore | 12 months ago | |
| Dockerfile | 4 days ago | |
| LICENSE | 1 year ago | |
| Makefile | 5 days ago | |
| Makefile.hub | 5 days ago | |
| README.md | 3 days ago | |
| compose.hub.yaml | 4 days ago | |
| compose.override.yaml | 5 days ago | |
| compose.prod.yaml | 9 months ago | |
| compose.yaml | 4 days ago | |
| composer.json | 4 days ago | |
| composer.lock | 4 days ago | |
| importmap.php | 5 days ago | |
| package.json | 1 year ago | |
| phpunit.xml.dist | 1 year ago | |
| symfony.lock | 4 days ago | |
README.md
Unfold: Imwald
A Symfony + FrankenPHP site that reads Nostr long-form articles (kind 30023) and related data from relays, and serves pages with Twig.
Where data lives
| Data | Storage |
|---|---|
| Published articles (30023/24) | MySQL article table (from articles:get / relay sync) |
| Magazine index (30040), kind-0 profiles, NIP-65 relay lists (10002) | MySQL event table with stable core_row_key (filled by app:prewarm and on-demand fetches) |
| Comment / reply / thread UI (fetched thread HTML, etc.) | Filesystem cache pool cache.replies (not the DB) |
| Unpublished editor preview payloads | Filesystem cache pool cache.drafts |
Generic Symfony cache.app |
Other app caches; not used for long-term profile or magazine index storage |
NIP-09 kind-5 deletions that target stored kinds are applied to MySQL (articles + event rows). Relays are expected to handle ephemeral thread data.
Requirements
| Requirement | Version / notes |
|---|---|
| PHP | ≥ 8.3.13 (see composer.json) |
| Docker | Optional; recommended for local dev and production images |
| Database | MySQL 8.0 (configurable) |
Local development (Docker)
-
Env: copy
.env.distto.envand adjust if needed (especiallyAPP_SECREToutside dev). -
Start stack
docker compose up -d -
App URL (default): http://127.0.0.1:9080
Port comes fromHTTP_PORTin.envandcompose.override.yaml(loopback only). -
First-time DB: migrations run on php container start when
migrations/contains PHP files (seefrankenphp/docker-entrypoint.sh).
| Service | Role |
|---|---|
php |
FrankenPHP + Caddy, Symfony app, console |
database |
MySQL; dev exposes 127.0.0.1:3307 → 3306 for local clients |
cron |
Runs full app:prewarm every 10 minutes; repo bind-mounted at /var/www/html (see docker/cron/) |
Backfill articles + prewarm (recommended)
To migrate, import articles from Nostr for a time window, then run prewarm (magazine + profiles + deletions + comment cache):
make prewarm
| Step (script order) | Command / effect |
|---|---|
| 1 | docker compose up -d --wait — starts php, database, and cron (the cron image runs a full app:prewarm on a 10 min schedule) |
| 2 | doctrine:migrations:migrate — applies schema (including event columns for core Nostr rows) |
| 3 | articles:get -- '-2 month' 'now' — sync long-form into the article table |
| 4 | app:prewarm — NIP-09 kind-5 sync (for stored kinds), magazine 30040 → event, kind-0 profiles (and relay lists on demand) → event, comment thread cache → cache.replies (default --comments-max=10, newest by createdAt) |
make prewarm brings the stack (including cron) up so scheduled prewarm is active. Optional extra arguments for the cron-scheduled app:prewarm go in .env as PREWARM_FLAGS (same as you might pass to php bin/console app:prewarm …); Compose passes them into the cron container. Example: PREWARM_FLAGS="--metadata-limit=50 --no-magazine". Restart the cron service after changing PREWARM_FLAGS so the container reloads the env. On the hub stack, the prewarm service reads the same PREWARM_FLAGS; use docker compose -f compose.hub.yaml up -d --force-recreate prewarm after changing it.
Fresh database or major upgrade: after schema changes, run articles:get + app:prewarm (or make prewarm) so article and event are repopulated from relays. There is no automatic migration of old PSR profile cache into MySQL.
Console commands (overview)
| Command | Purpose |
|---|---|
articles:get <from> <to> |
Pull long-form articles from Nostr for the time range, persist to article |
app:prewarm |
Magazine 30040 + kind-0 profile prewarm (→ event), NIP-09 deletions, comment thread warm (→ cache.replies) |
doctrine:migrations:migrate |
Apply SQL migrations |
user:elevate |
(If used) user elevation helper |
php bin/console list and … -h for full options.
app:prewarm (notable options)
| Option | Default | Meaning |
|---|---|---|
--no-magazine |
off | Skip magazine 30040 index fetch / event update |
--no-metadata |
off | Skip batched kind-0 profile prewarm (writes to event) |
--no-deletions |
off | Skip NIP-09 kind-5 fetch and application (articles + event index/profile rows) |
--deletion-since |
-2 month |
strtotime() lower bound for kind-5 author-scoped fetch |
--no-comments |
off | Skip comment thread prewarm (cache.replies) |
--metadata-limit |
0 (all authors) |
Max distinct author pubkeys for the metadata phase |
--metadata-batch |
50 |
Pubkeys per batched kind-0 Nostr REQ |
--comments-max |
10 |
Newest N articles (by createdAt DESC); 0 = all (still bounded by budget) |
--comments-budget |
600 |
Max wall seconds for the whole comments phase (Nostr is slow; raise e.g. 1200 if you need more articles in one run) |
--magazine-budget |
90 |
Max wall seconds for magazine per-category 30040 fetches (root is separate; cap 600s in code). If you have many categories, a low budget can stop before the last slug is refreshed. Set MAGAZINE_PREWARM_PREFER_SLUGS (comma-separated category #d slugs) to fetch those first after the root. |
Prewarm clears the PHP CLI execution time limit for that run; relay work can be slow.
PREWARM_ON_START (optional)
| Variable | Set where | Effect |
|---|---|---|
PREWARM_ON_START=1 |
Compose environment on the php service (not only Symfony .env inside the container) |
After DB is up and migrations run, executes app:prewarm once on start. Does not run articles:get. |
For a full Nostr backfill + one-shot prewarm, use make prewarm (or a host cron / systemd timer) instead of relying on PREWARM_ON_START alone.
Configuration
| What | File |
|---|---|
Site title, npub, d_tag, relays (default_relay, article_relays, profile_relays), theme |
config/unfold.yaml (imported as Symfony parameters) |
MAGAZINE_PREWARM_PREFER_SLUGS |
.env / .env.local — optional comma-separated category slugs to prioritize in app:prewarm magazine phase (after the root). Use when the relay time budget would otherwise skip your updated category. |
DATABASE_URL, APP_SECRET, HTTP_PORT, MYSQL_*, optional PREWARM_FLAGS (for the Docker cron service) |
.env / .env.local (see .env.dist) |
Cache pool definitions (cache.replies, cache.drafts, cache.app) |
config/packages/cache.yaml |
| Service wiring (e.g. which pool comment loaders use) | config/services.yaml |
Relays (short): default_relay and article_relays drive article sync and many queries; profile_relays are used first for kind-0 / profile fetches, then the merged default + article set (see NostrClient).
Production / Hub (remote server)
The app runs as a pre-built image (no app source on the server). The server only needs compose.hub.yaml, a .env, and Docker. Default image: silberengel/unfold:latest; override with UNFOLD_DOCKER_IMAGE.
| Topic | Notes |
|---|---|
compose.hub.yaml |
Defines php (FrankenPHP) + database (MySQL) + prewarm (same app image: app:prewarm every 10 minutes, like dev’s docker/cron). Optional: disable prewarm in Compose if you prefer a host cron only. |
| HTTP | HTTP_PUBLISH in .env maps host port → container 80 (default 9080). Put a reverse proxy (e.g. Apache) in front; set TRUSTED_PROXIES to match your proxy (often include 127.0.0.0/8 and the Docker bridge CIDR, e.g. 172.16.0.0/12). |
| Secrets | Real APP_SECRET and MYSQL_* (or external DB via DATABASE_URL if you change the file). Do not commit production .env. |
PREWARM_FLAGS |
Optional extra CLI args for the hub prewarm service (and dev cron). After editing .env, run docker compose -f compose.hub.yaml up -d --force-recreate prewarm. |
Build, tag, and push (on your machine or CI)
From the repository root (same Dockerfile as local prod):
# Production image
docker build --platform linux/amd64 --target frankenphp_prod -t YOUR_REGISTRY/unfold:latest .
# Optional: immutable tag for rollbacks
docker build --platform linux/amd64 --target frankenphp_prod -t YOUR_REGISTRY/unfold:1.0.0 .
# Push what you use on the server
docker push YOUR_REGISTRY/unfold:latest
docker push YOUR_REGISTRY/unfold:1.0.0
- Use
linux/amd64if the server is amd64; usearm64(or a matching--platform) for arm servers. - The image name must match what the server will pull: either keep
UNFOLD_DOCKER_IMAGE=YOUR_REGISTRY/unfold:TAGin server.env, or push to the default namesilberengel/unfold:latest.
Deploy on the server (pull, up, migrate)
In a directory that contains only compose.hub.yaml and your .env (e.g. ~/tmp/unfold):
cd /path/to/deploy
docker compose -f compose.hub.yaml pull
docker compose -f compose.hub.yaml up -d
docker compose -f compose.hub.yaml exec php php bin/console doctrine:migrations:migrate --no-interaction
After code changes: pull → up -d; run migrations when the repo added new migration files.
Makefile.hub (on the server)
Copy Makefile.hub into the same directory as compose.hub.yaml and .env (no full clone required). You get short commands like the dev Makefile, all using docker compose -f compose.hub.yaml under the hood:
make -f Makefile.hub help # list targets
make -f Makefile.hub pull
make -f Makefile.hub up
make -f Makefile.hub migrate
make -f Makefile.hub prewarm-once
make -f Makefile.hub articles-get # optional: ARTICLES_FROM='-1 year' ARTICLES_TO=now
make -f Makefile.hub backfill # up + migrate + articles-get + prewarm-once (closest to dev `make prewarm`)
Optional image / tag (in .env or one-shot):
export UNFOLD_DOCKER_IMAGE=YOUR_REGISTRY/unfold:1.0.0
docker compose -f compose.hub.yaml up -d
One-time Nostr backfill (equivalent to make prewarm on dev)
Use make -f Makefile.hub backfill, or run the same inside the php container:
docker compose -f compose.hub.yaml exec -T php php bin/console articles:get -- '-2 month' 'now'
docker compose -f compose.hub.yaml exec -T php php bin/console app:prewarm
Adjust the articles:get window as needed (see Makefile.hub / ARTICLES_FROM / ARTICLES_TO).
Scheduled app:prewarm on hub
The prewarm service uses the same image as php and runs app:prewarm every 10 minutes (same cadence as dev’s docker/cron). It waits for MySQL and the doctrine_migration_versions table (so the php entrypoint has run migrations) — it does not use curl to the php service, which can fail if HTTP is only bound for loopback inside that container. Optional PREWARM_FLAGS in .env is passed into that container; after changing it, run:
docker compose -f compose.hub.yaml up -d --force-recreate prewarm
If you do not want a Compose sidecar (e.g. to save RAM), stop and disable the prewarm service and use host cron or systemd instead:
*/10 * * * * cd /path/to/deploy && docker compose -f compose.hub.yaml exec -T php php bin/console app:prewarm
PREWARM_ON_START=1 on the php service only warms once at container start, not on a schedule.
The file compose.hub.yaml in the repo repeats minimal pull/migrate/build one-liners in its header for quick copy-paste.
License
MIT — see LICENSE.
Project links (example)
Configurable under parameters.external_links in config/unfold.yaml (e.g. Unfold on GitHub, Decent Newsroom). Adjust for your deployment.