# Twigpine node — environment variables # Copy to .env and fill in your values. # All variables are optional unless marked REQUIRED. # ── Node identity ───────────────────────────────────────────────────────── # Path to the node's Ed25519 keypair PEM file. Must name a key file, not a # directory: a trailing slash, a final `.` or `..`, and `..` traversal are # refused so the path cannot retarget a parent. A `~/` value must stay under # home after expansion. Bare `identity.pem` is still legal. # Generate with: gl identity new GITLAWB_KEY=/data/keys/identity.pem # Path to the node's persistent libp2p identity key file. Must include a # directory, because the node will not keep its p2p identity key in the working # directory. A value that cannot name a securable key file (a bare filename, # `..` traversal, a trailing slash, the filesystem root, or a `~/` path that # escapes home) is refused before the node binds, so the process exits. # Anything wrong with the storage itself (a symlinked or non-directory parent, # an unsafe cwd or ancestor, an unreadable or malformed key) leaves p2p off # while HTTP keeps serving, logged at error as p2p_identity_key_load_failed and # counted by the gitlawb_p2p_identity_key_load_failed metric. /health still # reports healthy in that state, so alert on the event or the metric. Fixing # the storage and restarting restores the same PeerId. # On Unix it is created 0600 inside a 0700 directory. A key directory that # grants access beyond its owner is tightened to 0700 on start; one closed too # far is refused with a chmod 700 remedy rather than widened. On other # platforms no permissions are enforced. # If the node logs that it tightened a key directory that was GROUP OR WORLD # WRITABLE, treat the key that was sitting there as possibly exposed: delete # it so a fresh identity is generated on the next start. An ordinary 0755 # directory is tightened too, but others could not read a 0600 key or replace # its directory entry, so do not rotate for that case. A directory that was # merely closed too far is refused, not tightened, and no key in it was ever # exposed. # Keep it on a persistent volume so the PeerId survives redeploys. # Default: ~/.gitlawb/p2p.key #GITLAWB_P2P_KEY=/data/keys/p2p.key # Publicly reachable URL of this node (used in peer announcements) GITLAWB_PUBLIC_URL=https://your-node.example.com # Base URL for the web view of repos on this node (used by `gl` to print a # working View: link after repo creation). Omit for nodes with no web front-end. # Distinct from GITLAWB_PUBLIC_URL: that is API reachability for peers; # this is browser reachability for humans. # GITLAWB_WEB_URL=https://gitlawb.com # ── Server ──────────────────────────────────────────────────────────────── GITLAWB_HOST=0.0.0.0 GITLAWB_PORT=7545 # ── Storage ─────────────────────────────────────────────────────────────── GITLAWB_REPOS_DIR=/data/repos # PostgreSQL connection URL. Required. # When using the bundled docker-compose, this is wired automatically. DATABASE_URL=postgresql://gitlawb:changeme@localhost:5432/gitlawb # ── Database pool & startup resilience ──────────────────────────────────── # Maximum connections in the PostgreSQL pool. A cap, not a floor — # connections open lazily. Size against the DB server's max_connections, # remembering admin tooling opens its own pool. GITLAWB_DB_MAX_CONNECTIONS=48 # Maximum connections in the DEDICATED advisory-lock pool, separate from the # pool above. Every in-flight repo write pins one connection here for its whole # duration, so this is a hard ceiling on simultaneous writes node-wide: size it # to expected peak concurrent writers, not small. Keeping it separate is what # stops a push burst from starving ordinary request handlers. Budget # (GITLAWB_DB_MAX_CONNECTIONS + this) per node against the server's # max_connections, times node count, plus admin tooling. GITLAWB_DB_LOCK_POOL_MAX_CONNECTIONS=40 # Upper bound, in seconds, on any object-storage transfer that runs while a # per-repo write lock is HELD (the archive download inside acquire_write and the # upload inside release). These were free before the lock's connection was # pinned to the guard; now an unbounded stall holds a lock-pool slot, and enough # stalls deny every write on the node. The bound applies PER SPAN and there are # two (the acquire-side refresh, which covers the existence check and download # together, and the release-side upload), so worst-case slot occupancy is about # twice this value plus the git work between them. Read it together with the pool # size above and with GITLAWB_GIT_SERVICE_TIMEOUT_SECS. GITLAWB_LOCK_HELD_TRANSFER_TIMEOUT_SECS=300 # Seconds a request waits for a pool connection before failing with 503. GITLAWB_DB_ACQUIRE_TIMEOUT_SECS=5 # Upper bound on each startup connect+migrate attempt, in seconds. Keep it # generous enough for a peer instance to finish migrating (the migration # advisory lock is held across a deploy); on expiry the attempt is retried. GITLAWB_DB_CONNECT_TIMEOUT_SECS=60 # Startup retry backoff while the database is unavailable: first delay and # ceiling, in seconds. The node stays up serving 503s (with a truthful # /ready) instead of crash-looping. GITLAWB_DB_RETRY_INITIAL_SECS=5 GITLAWB_DB_RETRY_MAX_SECS=60 # ── IPFS pinning (Pinata) ───────────────────────────────────────────────── # Get a JWT at https://app.pinata.cloud/developers/api-keys GITLAWB_PINATA_JWT= GITLAWB_PINATA_UPLOAD_URL=https://uploads.pinata.cloud/v3/files # ── Arweave permanent anchoring (Irys devnet) ───────────────────────────── # Leave empty to disable Arweave anchoring. GITLAWB_IRYS_URL=https://devnet.irys.xyz # ── Base L2 smart contracts ─────────────────────────────────────────────── GITLAWB_CHAIN_RPC_URL=https://sepolia.base.org GITLAWB_CONTRACT_DID_REGISTRY=0x8046284116C5ac6724adbBf860feBeA85692d574 GITLAWB_CONTRACT_NAME_REGISTRY=0x73094B9DAb2421878A20Abed1497001fbD51302c # $GITLAWB ERC20 token (Base mainnet). GITLAWB_TOKEN=0x5F980Dcfc4c0fa3911554cf5ab288ed0eb13DBa3 # ── On-chain PoS (operators — required to earn rewards) ─────────────────── # Set both of the next two to enable Proof-of-Stake mode. With both set, the # node verifies its registration on startup and posts heartbeats automatically. # Leave empty to run without on-chain PoS (local dev / pre-launch). # Deployed GitlawbNodeStaking contract address. GITLAWB_CONTRACT_NODE_STAKING= # Operator wallet private key (0x-prefixed hex). Used to sign heartbeats. # Use a dedicated wallet with only enough ETH for gas — not your treasury. GITLAWB_OPERATOR_PRIVATE_KEY= # If true, the node refuses to start unless registered and currently active. # Leave false while bootstrapping; flip to true once your stake is confirmed. GITLAWB_OPERATOR_STRICT_MODE=false # Heartbeat cadence in hours. Must be < 24 with headroom. Default: 20. GITLAWB_HEARTBEAT_INTERVAL_HOURS=20 # ── docker-compose Postgres password ────────────────────────────────────── # Only used when running via `docker compose up` with the bundled Postgres. POSTGRES_PASSWORD=changeme # ── P2P networking ──────────────────────────────────────────────────────── GITLAWB_P2P_PORT=7546 # Comma-separated HTTP peer URLs to announce to on startup. # The binary also ships with bootstrap-peers.json; set # GITLAWB_BOOTSTRAP_DISABLE_SEEDS=true for isolated dev/test networks. GITLAWB_BOOTSTRAP_PEERS= GITLAWB_BOOTSTRAP_DISABLE_SEEDS=false # Require a peer write to prove its DID on every transport: RFC 9421 signatures # on the announce/sync routes, and an Ed25519 payload signature on gossip # ref-update events. Keep false during rolling upgrades so existing live nodes # can still communicate. Upgrade every gossip-publishing peer to a build that # signs events before setting this true, or their updates are dropped on # arrival with no error visible on their side. GITLAWB_REQUIRE_SIGNED_PEER_WRITES=false # Require the authenticated pusher to be the repo owner on git-receive-pack. # A valid did:key signature is authentication, not authorization: anyone can # mint a key, derive its DID and sign, so with this off every signed caller may # push to every repository, private ones included. On by default. # # Set to false only for a rolling upgrade whose pushers are not yet the repo # owner. Note that delegated and CI keys count as non-owners: UCAN git/push is # verified but not yet honored for authorization, so they cannot push while this # is on. See docs/RUN-A-NODE.md. GITLAWB_ENFORCE_OWNER_PUSH=true # Comma-separated libp2p multiaddrs. # Example: /ip4/1.2.3.4/udp/7546/quic-v1/p2p/12D3KooW... GITLAWB_P2P_BOOTSTRAP= # ── Access control ──────────────────────────────────────────────────────── # Reserved and currently inert. Repo reads are gated per repository by # is_public and path-scoped visibility rules; this flag changes nothing. GITLAWB_PUBLIC_READ=true # Maximum git smart-HTTP pack request size, in bytes. GITLAWB_MAX_PACK_BYTES=2147483648 # Max seconds a served git upload-pack / receive-pack (clone / push) may run # before it is aborted with a 504. Bounds a hung git that would otherwise pin a # worker and, on push, the repo write lock. Also bounds both info/refs # advertisements, the withheld-blob pack build, and the push-side candidate # discovery (rev-list / cat-file), all reaped via process-group teardown (#174). # On the path-scoped upload-pack path the withheld-blob classification walk and # the pack serve share ONE deadline, so this value bounds their COMBINED duration # rather than giving each stage a full budget: a walk that consumes it leaves the # serve nothing and the clone gets a 504. Serving large path-scoped repos may # need a higher value here than when each stage was budgeted separately. # Must be 1..=3153600000 (100 years): the node derives deadlines from this value, # and a larger one cannot be represented. Default 600. GITLAWB_GIT_SERVICE_TIMEOUT_SECS=600 # Max seconds the storage-ACQUISITION phase of a served git op may run before the # request is shed with a 503, separate from the git-run timeout above. A # concurrency permit is taken before this phase and GITLAWB_GIT_SERVICE_TIMEOUT_SECS # only starts once git spawns, so without this a stalled backend (a hung Tigris # HEAD/GET, or a hung pg advisory-lock iteration on push) pins the permit and drains # the pool until every later request 503s. On expiry the permit is released # (fail-closed). Kept separate because acquisition and git execution are distinct # cost centers. Must be positive; set very large to effectively disable. Default 30. GITLAWB_GIT_ACQUIRE_TIMEOUT_SECS=30 # Max concurrent git READ ops (upload-pack + the upload-pack info/refs # advertisement) served at once, a global pool separate from the push pool below. # The anon receive-pack info/refs advertisement has its OWN pool (see below), not # this one. Over-cap sheds a clean 503 + Retry-After. Anonymous reads draw from # here, so pair it with GITLAWB_MAX_CONCURRENT_READS_PER_CALLER (below) so one # caller cannot monopolize the pool. Default 128. GITLAWB_MAX_CONCURRENT_GIT_OPS=128 # Max concurrent git-receive-pack (push) POST operations, in a pool separate from # the read pool (GITLAWB_MAX_CONCURRENT_GIT_OPS) so anonymous reads cannot shed an # authenticated push at admission. The anon receive-pack info/refs advertisement # runs in a SEPARATE pool of the same size (disjoint from this one), so an # advertisement flood cannot shed a push either. Over-cap sheds a 503 + # Retry-After. Default 32. GITLAWB_MAX_CONCURRENT_GIT_PUSHES=32 # Max pushes PARKED at once waiting for the per-repo write lease of the SAME repo. # A parked push holds its already-buffered pack body in memory, so this bounds that # memory for a hot repo. Past the cap the newest push sheds a clean 503 + # Retry-After ("repo is busy with another push") instead of joining the queue; a # push to a DIFFERENT repo is never affected. The lease holder is not counted. # Raising GITLAWB_MAX_CONCURRENT_GIT_PUSHES does not raise this; set it explicitly. # Default 8. GITLAWB_REPO_LEASE_MAX_WAITERS=8 # Max concurrent post-push pin loops (IPFS + Pinata pin_new_objects) across all # repos. The per-repo encrypt-task set caps the pin-task COUNT; this caps how many # of those loops RUN CONCURRENTLY, and so how much MB-scale object-id list memory is # held WHILE BEING PINNED. It does not bound retained memory in general: on the local # IPFS path the caller builds its list before this pool is entered, so a loop parked # waiting for a permit still holds its full list, and how many park is capped only # per repo. Do not size memory from this knob alone. A loop DEFERS (waits) when the # pool is full, never drops a pin. Default 8. GITLAWB_MAX_CONCURRENT_PIN_TASKS=8 # Max concurrent read ops (upload-pack + the upload-pack info/refs advertisement) # a single caller may hold, so one caller cannot monopolize the read pool. Keyed # on the resolved SOURCE IP, never the DID: a signature does not move a caller off # this cap. The source-IP key is only as granular as GITLAWB_TRUSTED_PROXY below: # left unset, a node behind an edge/NAT keys all callers on the edge IP and this # collapses to one global cap. Set GITLAWB_TRUSTED_PROXY for per-client keying; a # high-fanout caller (CI behind one NAT) then needs the operator to raise this. # Default 16. GITLAWB_MAX_CONCURRENT_READS_PER_CALLER=16 # Two further per-source concurrency caps exist on the PUSH side but have NO # environment variable of their own: both are DERIVED as # GITLAWB_MAX_CONCURRENT_GIT_PUSHES / 8, with a floor of 1. They are not settable # independently, so raising the push pool raises both proportionally, and the # default push pool of 32 gives each a cap of 4. # * The anon receive-pack info/refs advertisement cap: one source IP may hold at # most this many slots in the dedicated advert pool, so saturating that pool # takes ~8 distinct source IPs. # * The authenticated receive-pack POST cap: one source IP may hold at most this # many WRITE-pool slots. This one is load-bearing for push availability, since # it is acquired before the global write permit: without it, one host minting # disposable did:key identities could open enough slow pushes to monopolize the # write pool and 503 every other source (the push rate limiter caps arrival # rate, not in-flight concurrency, and owner enforcement can be turned off for # a rolling upgrade). # Keyed on the resolved source IP, never the DID, so a DID farm does not defeat # them; keying granularity follows GITLAWB_TRUSTED_PROXY like the read cap above. # ── Push rate limiting (git-receive-pack flood brake) ───────────────────── # Max receive-pack requests (info/refs advertisement + push POST) per client # IP per hour. 0 disables. Default 600. GITLAWB_PUSH_RATE_LIMIT=600 # ── /ipfs/{cid} visibility-walk admission (#174) ────────────────────────── # GET /ipfs/{cid} runs a per-repo full-history git walk in a blocking thread to # decide whether the caller may read a path-scoped blob. It is publicly reachable, # so it is bounded to keep a permissionless caller from fanning out unbounded # concurrent walks and exhausting blocking-pool threads + PIDs. # Max concurrent /ipfs walks across all callers (a pool of its own, disjoint from # the served-git pools). Over-cap sheds a 503. Default 32. GITLAWB_MAX_CONCURRENT_IPFS_WALKS=32 # Max concurrent /ipfs walks a single SOURCE IP may hold (keyed like the git # per-caller caps via GITLAWB_TRUSTED_PROXY; reject-before-insert bounded map). # Default 4. GITLAWB_IPFS_WALK_PER_SOURCE=4 # Max legacy (NULL-provenance) repos probed per single /ipfs request, bounding the # scan-fallback fan-out (git cat-file per candidate repo) for an anonymous caller. A # truncated scan sheds a retryable 503, never a false 404. Default 256. GITLAWB_IPFS_MAX_LEGACY_PROBES=256 # Max repo ROWS one /ipfs request's legacy scan may read from the database. The probe # ceiling above only counts once a probe runs, and quarantined or private repos are # denied before that, so without this an all-denying inventory paged the whole repo # table for one anonymous request. A truncated scan sheds a retryable 503 carrying a # sealed `continuation` token; echo it as ?scan= to resume, so a holder buried past the # ceiling is reachable in ceil(repos / ceiling) + 1 requests. Raising this also raises # the per-caller /ipfs work allowance, since each page is charged to it. Lowering it # sharpens a coarse oracle: laddering to the end reveals the node's total repo count to # within one ceiling. Default 2048. GITLAWB_IPFS_MAX_LEGACY_SCAN_ROWS=2048 # Max EXPENSIVE path-scope visibility walks per single /ipfs request (only a # blob in a path-scoped repo costs a full-history walk). Over-cap repos are # skipped without a verdict and the scan continues; if the object is then found # nowhere the request sheds a retryable 503 instead of a false 404. The effective # cap is the tighter of this value and the node's internal per-request ceiling of # 17, so values above 17 have no effect; lower values do tighten it. Default 64. GITLAWB_IPFS_MAX_REPOS_WALKED=64 # Ceiling on repos one /ipfs request may VISIT past the visibility gate. Each # visit costs a repo acquire — on a Tigris cache miss a full archive download, # so this is also the worst-case object-store fetch count per request — plus a # cat-file probe. On exhaustion the scan stops and sheds a retryable 503. # Default 1024. GITLAWB_IPFS_MAX_REPO_VISITS=1024 # Absolute wall-clock budget for one admitted /ipfs request's acquire+walk # lifetime (all stages of the whole scan). Per-stage clamps bound the acquire # and walk stages to the remaining budget, and no stage starts once it is # exhausted (the scan then sheds a retryable 503). The object-type probe and # content-read cat-file subprocesses are budget-checked before starting AND each # run under their own deadline (the lesser of GITLAWB_GIT_SERVICE_TIMEOUT_SECS and # the remaining budget), reaped via process-group teardown, so a hung cat-file # cannot hold the request's walk slot past it. One hang path is still unbounded: # the probe's object-store readability check is a plain filesystem sweep with # nothing to reap, so a wedged filesystem can hold the slot past the deadline. # Must be 1..=3153600000 (100 years): the node derives an Instant deadline from # this value, and a larger one cannot be represented. Default 600. GITLAWB_IPFS_REQUEST_BUDGET_SECS=600 # Shorter budget (seconds) for the pre-walk CID resolve: the lookup that maps a # requested CID to its git oid(s), which runs while the scarce walk admission is # already held. A well-formed CID with no pin row does no probe and no walk work, # so without this a stalled lookup could hold a walk slot for the whole request # budget while nothing walked. The effective deadline is the lesser of this and # the remaining request budget; walk and probe work stay on the request budget, # so a slow but progressing scan is never shed by it. # Must be 1..=3153600000 (100 years). Default 10. GITLAWB_IPFS_RESOLVE_BUDGET_SECS=10 # Max /ipfs/{cid} requests per client IP per hour (route flood brake, distinct # from the concurrency caps above). 0 disables. Default 600. GITLAWB_IPFS_RATE_LIMIT=600 # Per-client-IP rate limit for the anonymous task read routes # (GET /api/v1/tasks, GET /api/v1/tasks/{id}), in requests per hour. # Both are publicly reachable (optional_signature). GET /api/v1/tasks runs # collect_visible_tasks and returns a visibility-filtered page; # GET /api/v1/tasks/{id} runs get_visible_task and returns an opaque 404 when # the task is hidden or missing. These reads can require task, repository, # and visibility-rule queries even when no task is returned, so the brake # bounds the cost of anonymous probes. Keyed on the resolved client IP via # GITLAWB_TRUSTED_PROXY. 0 disables. Default: 1200 (a list page # followed by per-task reads is a normal client pattern, so this sits above # the /ipfs budget). The GraphQL/WS task-read brake shares this budget. GITLAWB_TASK_READ_RATE_LIMIT=1200 # ── Creation rate limiting (repo/agent/issue/PR flood brake) ────────────── # Max creation requests (POST /api/v1/repos, /api/register, fork, issues, # pulls) per client IP per hour, in addition to the per-DID limit. The per-DID # limit alone is bypassed by a did:key farm (one throwaway identity per repo), # which is how spam-repo floods slip past it and the iCaptcha gate; the per-IP # brake caps a single-source flood. Uses GITLAWB_TRUSTED_PROXY below to resolve # the client IP. 0 disables. Default 120. GITLAWB_CREATE_RATE_LIMIT=120 # Per-DID creation requests allowed inside the window below (repo create, # issue create, PR create, register, fork). 0 is rejected at startup: it would # silently disable the limiter. Default 10. GITLAWB_RATE_LIMIT_MAX=10 # Window the per-DID creation count above applies over, in seconds. 0 is # rejected at startup: every timestamp would prune immediately, disabling the # limiter. Default 3600. GITLAWB_RATE_LIMIT_WINDOW_SECS=3600 # ── Peer-sync rate limiting (per client IP, uses GITLAWB_TRUSTED_PROXY below) ─ # /api/v1/peers/announce and /api/v1/sync/notify accept unsigned requests from # known peers and run at higher frequency, so a generous bucket. Separate from # the trigger bucket so an unsigned notify flood can't drain trigger's quota. # 0 disables. Default 600. GITLAWB_PEER_WRITE_RATE_LIMIT=600 # /api/v1/sync/trigger requires a signature and fans out to every peer per call, # so it gets a tight bucket. 0 disables. Default 60. GITLAWB_SYNC_TRIGGER_RATE_LIMIT=60 # Which forwarded header the edge is trusted to set, used to resolve the real # client IP for the push limiter. One of: # (unset) — no trusted proxy: key on the socket peer address, ignore headers. # fly — behind Fly's edge (trust Fly-Client-IP). # x-forwarded-for — behind a single reverse proxy like Caddy/NGINX (trust the # rightmost X-Forwarded-For hop). # Only set this when a proxy you control actually fronts the node; trusting a # forwarded header on a directly-exposed node lets clients spoof the key. GITLAWB_TRUSTED_PROXY= # ── Sync ───────────────────────────────────────────────────────────────── # Enable automatic background sync from known peers GITLAWB_AUTO_SYNC=false # ── External upstream mirroring ─────────────────────────────────────────── # Default-off scheduled fetch worker for repositories configured as INBOUND. # Enabling requires GITLAWB_ENFORCE_OWNER_PUSH=true; the node refuses to start # otherwise. The worker updates branches and tags atomically while preserving # Gitlawb-owned review/internal refs. Enabling also requires Git >= 2.37 on # the host for http.curloptResolve DNS pinning; the node refuses to start # when the installed Git is older. GITLAWB_UPSTREAM_MIRROR_ENABLED=false # Pause after each complete scan. Must be 5..=86400 seconds. GITLAWB_UPSTREAM_MIRROR_INTERVAL_SECS=300 # Deadline for the external fetch child only; the preflight probes run on # their own short bound. Must be 1..=3153600000 seconds. GITLAWB_UPSTREAM_MIRROR_FETCH_TIMEOUT_SECS=600 # Keyset page size; every page is processed before the next scan delay. GITLAWB_UPSTREAM_MIRROR_PAGE_SIZE=100 # ── Reconciliation sweep ───────────────────────────────────────────────── # Periodic durability sweep: re-derives the public pin set and the withheld-blob # recovery set each hour and fills gaps so a dropped replication job never means # data loss. Defaults to true; set to false to disable the sweep even when a pin # backend (IPFS/Pinata) is configured. # # Phase-capability matrix: # - Public pin repair: IPFS-only, Pinata-only, or both (requires the # respective backend to be configured). # - Encrypted recovery repair: requires local IPFS (GITLAWB_IPFS_API). # Pinata-only nodes reconcile public pins only; encrypted recovery # reconciliation is not performed. GITLAWB_RECONCILIATION_SWEEP=true # ── iCaptcha proof-of-intelligence gate ─────────────────────────────────── # Optional gate on create_repo + register: require callers to present an # iCaptcha proof (X-ICaptcha-Proof header) earned at icaptcha.gitlawb.com. # Default off = fully inert (no behavior change). # off - gate disabled (default) # shadow - verify + log would-be rejections, but always allow # enforce - reject requests without a valid, strong-enough proof ICAPTCHA_MODE=off # Optional base64url Ed25519 public key override; if unset it is fetched from # ICAPTCHA_URL/v1/pubkey at startup. Recommended in production: pinning the key # removes the startup dependency on the iCaptcha service being reachable and # avoids silently running inert if that one-time fetch fails. ICAPTCHA_PUBKEY= # Minimum proof difficulty level required to pass. ICAPTCHA_REQUIRED_LEVEL=3 # Base URL of the iCaptcha service. ICAPTCHA_URL=https://icaptcha.gitlawb.com