An open protocol concept for cities to discover, exchange and trace material and product flows across city boundaries.
This monorepo holds the LOOP specification and schemas, the public documentation hub, and the lab-only backend API.
Live site · Specification · API docs · Quickstart · Architecture · Examples · Contributing · Licensing
Important
localLOOP is an early-stage, low-TRL research concept. Everything here is a lab demo only: there are no public pilots or deployments, no certified implementations, and the specification and schemas are drafts that may change.
- What is localLOOP?
- See it live
- Repository layout
- Architecture
- Requirements
- Quickstart
- Checks
- Examples
- Cross-tree contracts
- Deployment
- Limitations and status
- Documentation map
- Contributing and community
- Licensing
- Changelogs and releases
- History
LOOP (Local Optimization with Overflow Protocol) is a draft specification for a federation of city nodes. Each node keeps a registry of secondary materials and products and can offer what it cannot use locally to nearby nodes. Payloads are JSON-LD documents validated by JSON Schema. localLOOP is the project around it, and this repository contains three parts:
| Part | Path | What it is |
|---|---|---|
| LOOP protocol | protocol/ |
Canonical specification, JSON Schemas, JSON-LD contexts, RFCs, profiles, openapi.json |
| Documentation hub | apps/site/ |
Next.js static site at loop.urbnia.com with the spec, schemas, guides and a consent-based interest form |
| Lab backend API | apps/backend/ |
Bun + Fastify service at loop.urbnia.com/api/ running the minimal interop flow, interest registry and demo city data |
The minimal lab flow follows a material batch from registration to handoff:
flowchart LR
M["MaterialDNA<br/>register a material batch"] --> O["Offer<br/>publish availability"]
O --> X["Match<br/>accept a cross-city match"]
X --> T["Transfer<br/>record handoff and receipt"]
M -. "federate/announce" .-> P(["Peer node"])
T -. "loop events (SSE)" .-> S(["Subscribers"])
| Concept | Meaning |
|---|---|
| MaterialDNA | Identity and metadata of a material batch (MAT-… ids), optionally with passport-style fields |
| ProductDNA | Draft product-identity record that references the materials it is made from |
| Offer, Match, Transfer | The three exchange steps between nodes (spec §8.1) |
| Node | A federation peer; nodes exchange signed announcements (spec §8.2, §9.2) |
| Interest | Public expression of interest collected by the site (not part of the protocol) |
| City | Seeded demo data for the lab UI |
Read the full model in the specification and the glossary.
![]() |
![]() |
| Home | Specification |
![]() |
Browse the rendered docs The hub renders the specification, schemas, examples and guides with full styling, navigation and search-friendly URLs: |
| DEMO City |
Screenshots taken from the live site on 2026-10-14 at 1280 px wide. The site has a single light theme. The home page map uses OpenStreetMap data (© OpenStreetMap contributors, ODbL).
apps/
backend/ Bun + Fastify 5 API (plain SQL via pg, PostGIS, Valkey, SeaweedFS) → https://loop.urbnia.com/api/
site/ Next.js 16 static docs hub served by nginx → https://loop.urbnia.com
protocol/ LOOP specification, JSON Schemas, JSON-LD contexts, RFCs, profiles, openapi.json
ops/
ledger/ Agent ledger, repo manifest, verification evidence, plans (internal)
org-profile/ Source of the local-loop-io GitHub org profile (historical)
docs/assets/ README artwork: banner, architecture diagrams, screenshots
scripts/ Monorepo-wide tooling (ci-local.sh, check-links.mjs)
.github/ CI workflows (Gate on every PR, plus path-filtered per-tree workflows), Dependabot, CODEOWNERS
Each tree keeps its own toolchain and lockfile; there is no root package
manager workspace. Per-tree details live in
apps/backend/README.md,
apps/site/README.md and
protocol/README.md.
- One origin, two routers. On
https://loop.urbnia.comthe shared Traefik sends/api/*straight to the backend container and everything else to the site's nginx, which only serves the static export. The two stacks deploy independently. - Former hostnames
localloop.urbnia.comandloop-api.urbnia.comonly answer with 308 redirects toloop.urbnia.com/*andloop.urbnia.com/api/*. - Protocol first.
protocol/is the source of truth; the backend and the site carry checked copies (see Cross-tree contracts).
| Tool | Version | Needed for |
|---|---|---|
| Bun | 1.4.3 (the version in both Dockerfiles) | apps/backend, apps/site |
| Node.js | ≥ 24.19 (engines in every package.json; Volta pins 24.21) |
protocol scripts, site smoke tests, scripts/check-links.mjs |
| npm | bundled with Node | protocol (package-lock.json) |
| Docker with Compose v2 | recent | running the backend stack and building the images |
| PostgreSQL + PostGIS | 18 + 3.6 (postgis/postgis:18-3.6) |
backend data (started by compose) |
| Valkey | 9.2 (Redis-compatible) | backend queue (started by compose) |
| SeaweedFS | 4.48 (S3 gateway, host 127.0.0.1:9200) |
configured object store, no route uses it yet (started by compose) |
Note
Where npm is aliased to Bun, use npm run test rather than bare
npm test in protocol/; the bare form starts Bun's own test runner.
Run each tree from its own directory. Steps marked verified were run for this README on 2026-10-14; the others need services that were not started here.
Protocol: validate schemas and examples (verified)
cd protocol
npm ci
npm run test # AJV: schemas, examples, contexts, Core-DP vectors, README checks
npm run check:domains # banned-domain scanSite: build the static docs hub (verified)
cd apps/site
bun install
bun run build # static export to out/
bun run test # Node smoke tests (source checks + the built export)
bun run dev # dev server on http://localhost:3000 (not run here)bun run test:e2e builds and runs the Playwright suite (not run here). To
refresh the protocol mirror after a protocol change, run
bash apps/site/scripts/aggregate-docs.sh from the repository root.
Backend: type-check, or run the API locally (partly verified)
Verified (no database needed):
cd apps/backend
bun install
bun run typecheck && bun run lintRunning the API needs Postgres, Valkey and SeaweedFS. The compose file starts
them (and an api container) on 127.0.0.1 ports 55432, 6381, 9200 and 8088
(not run here):
cp .env.example .env # host-side development values
docker compose up -d # postgres, redis (Valkey), seaweedfs, api
bun run dev # API on :8088 (stop the compose api service first)
bun run lab:demo # migrate, seed and simulate the lab flow[!WARNING]
bun testapplies migrations to whateverDATABASE_URLpoints at. Point it at a scratch database (for examplelocalloop_test) and set an explicitREDIS_URLwith the password before running the test suite.
| Tree | Install | Check |
|---|---|---|
apps/backend |
bun install |
bun run typecheck && bun run lint && bun run check:conformance && bun test |
apps/site |
bun install |
bun run lint && bun run typecheck && bun run build && bun run test |
protocol |
npm ci |
npm run test |
| repository | none | node scripts/check-links.mjs (relative links and anchors in all Markdown) |
scripts/ci-local.sh runs the fast checks for every tree. The required
Gate workflow runs it on every pull request; run it locally before
pushing.
Health check. The public API answers on the same origin as the site:
$ curl -s https://loop.urbnia.com/api/health
{"status":"ok","time":"2026-10-14T21:55:49.606Z","uptime":2128.8,"db":"ok","redis":"ok","version":"0.6.4"}The response carries Cache-Control: no-store and returns 503 when the
database or Valkey probe fails.
Node metadata (JSON-LD, validated against node-info.schema.json):
$ curl -s https://loop.urbnia.com/api/v1/node/info
{"@context":"https://loop.urbnia.com/projects/loop-protocol/contexts/loop-v0.2.0.jsonld",
"@type":"NodeInfo","id":"lab-hub.loop","name":"localLOOP Lab Hub","version":"0.6.4",
"location":{"lat":48.1351,"lon":11.582,"city":"Munich","country":"DE"},
"endpoint":"https://loop.urbnia.com/api/v1","capabilities":["material-registry","loopsignal"],
"lab_only":true,"schema_version":"0.2.0"}(Line breaks added; version reports the deployed backend.)
A protocol payload. An Offer from
protocol/examples/06-offer.json, valid
against offer.schema.json:
{
"@context": "https://loop.urbnia.com/projects/loop-protocol/contexts/loop-v0.2.0.jsonld",
"@type": "Offer",
"schema_version": "0.2.0",
"id": "OFR-2F7A6B9C",
"material_id": "MAT-DE-MUC-2025-PLASTIC-B847F3",
"from_city": "Munich",
"to_city": "Berlin",
"quantity": { "value": 800, "unit": "kg" },
"status": "open",
"available_until": "2025-06-05T10:00:00Z",
"terms": "Ready for pickup within 48 hours"
}JSON-LD contexts and schema $ids are published under
https://loop.urbnia.com/projects/loop-protocol/ (since protocol 0.6.0).
More payloads: protocol/examples/. The full
endpoint list is in apps/backend/README.md.
protocol/is canonical. The backend keeps byte-identical schema copies inapps/backend/src/schemas/(bun run sync:schemas), and the site mirrors protocol artifacts intoapps/site/public/projects/loop-protocol/(bash apps/site/scripts/aggregate-docs.sh). The mirror keeps its public URL path, so JSON-LD contexts stay athttps://loop.urbnia.com/projects/loop-protocol/contexts/…(served via the redirect toloop.urbnia.com).bun run check:conformance(inapps/backend) fails on any drift between the three, and on spec endpoints missing from the API.- Adding RFC 2119 clauses to
protocol/SPECIFICATION.mdrequires re-pinningprotocol/profiles/core-dp/requirements/spec-v0.2.0-normative-manifest.json.
Both apps run with Docker Compose behind the host's shared Traefik. Compose
project names are pinned (localloop-backend, localloop-site), so
containers keep their historical names.
Routing and redeploy details
https://loop.urbnia.com→ site container (Traefik routerlocalloop-site).https://loop.urbnia.com/api/*→ backend containerlocalloop-backend-api-1:8088(Traefik routerlocalloop-api, labels inapps/backend/docker-compose.yml) over the dedicatedlocalloop-edgenetwork, which only Traefik and the API join. While that container is down the site's nginx answers/api/*with a JSON 503.- Limits: Traefik rate limits per client IP on every localLOOP router; the
API adds per-route read/write limits and per-IP stream caps (see
apps/backend/README.md). localloop.urbnia.com/*andloop-api.urbnia.com/*→ 308 redirects toloop.urbnia.com/*andloop.urbnia.com/api/*(labels inapps/site/docker-compose.yml).
cd apps/backend && docker compose up -d --build # API + Postgres + Valkey + SeaweedFS
cd apps/site && docker compose up -d --build # static site (nginx)Each stack deploys on its own; restarting the site no longer interrupts the API. Backend
secrets live in the git-ignored apps/backend/.env / .env.docker; data in
apps/backend/data/, nightly dumps in apps/backend/backups/.
- Lab demo only. No public pilots or deployments; the live node holds seeded example data. Outward-facing content must keep this wording (see claims and maturity).
- Low TRL drafts. The specification (v0.2.0) and schemas may change
without notice; there are no certified implementations. Spec coverage of
the API is tracked in
apps/backend/docs/SPEC-COMPLIANCE.md. Node-to-node signatures are checked for presence and freshness only. - One public monorepo. This repository replaces the former
github.com/local-loop-io/*repositories, which are archived and read-only (their history is included here). - Operations. Nightly database dumps stay on the host (nothing is copied
off-host yet) and a periodic restore drill is still an open follow-up. The
pilot-readiness claim in
protocol/docs/governance/pilot-readiness/expires on 2026-12-04.
| Topic | In this repository | Rendered |
|---|---|---|
| Specification | protocol/SPECIFICATION.md |
loop.urbnia.com/protocol/spec |
| Schemas and examples | protocol/schemas/, protocol/examples/ |
Library |
| RFCs and governance | protocol/rfcs/, GOVERNANCE.md |
Governance |
| Profiles (Core-DP and extensions) | protocol/profiles/ |
Docs |
| Implementation guide | protocol/docs/implementation-guide.md |
Implementation guide |
| API | apps/backend/README.md, openapi.json |
Redoc |
| Security guidance | protocol/docs/security-guide.md, threat model |
Security policy |
GitHub strips styles and scripts from README files, so the richer, styled version of this documentation is the live hub at loop.urbnia.com rather than a separate HTML copy in this repository.
- Contributing guide: workflow, checks and commit conventions.
- Security policy: how to report a vulnerability privately.
- Code of conduct.
- Governance charter and the RFC process for protocol changes.
- Interested in the concept? Use the expression-of-interest form; please do not submit sensitive information.
Copyright 2025-2026 Alphin Tom / Mycel UG (haftungsbeschränkt).
| What | Licence |
|---|---|
First-party code: apps/backend, apps/site, scripts, tooling |
MIT |
Protocol prose: SPECIFICATION.md, docs/, rfcs/ and other Markdown in protocol/ |
CC BY-SA 4.0 |
Protocol code and machine-readable artifacts: scripts, schemas, contexts, openapi.json |
MIT |
The localLOOP and LOOP names, logo, favicons, the six concept images, and the banner and screenshots in docs/assets/ |
All rights reserved (LicenseRef-Mycel-Brand) |
ops/ledger |
Proprietary, internal |
File-level licensing is recorded with SPDX headers and REUSE.toml. See
NOTICE for attribution and
THIRD_PARTY_NOTICES.md for bundled third-party
components.
Each tree is versioned and released on its own and keeps its own
Keep a Changelog file:
backend ·
site ·
protocol ·
org profile.
Repository-wide changes (layout, tooling, documentation) are recorded in the
root CHANGELOG.md. Tags are prefixed by tree, for example
backend/v0.6.4 or protocol/v0.5.2.
This repository consolidates the former local-loop-io repositories with
full history (each rewritten into its subdirectory):
| Former repo | Path | Tag prefix |
|---|---|---|
local-loop-io/localloop-backend |
apps/backend |
backend/ |
local-loop-io/localloop-site |
apps/site |
site/ |
local-loop-io/loop-protocol |
protocol |
protocol/ |
local-loop-io/.github |
ops/org-profile |
org-profile/ |
localloop-agent (local only) |
ops/ledger |
— |
The originals are archived. Unmerged work survives as legacy/* branches.


