Skip to content

forge-biz-apps + ERPNext

Self-hosted books/invoicing for the Wiebelhaus stack. Replaces QuickBooks ($80/mo → $0/mo) and is the canonical source of truth for chart of accounts, P&L, balance sheet, and invoicing across all entities.

LXC

  • Host: Finn (Proxmox), CTID 104
  • Hostname: forge-biz-apps
  • IP: 192.168.86.194 — pinned static in /etc/pve/lxc/104.conf (ip=192.168.86.194/24,gw=192.168.86.1) on 2026-06-08 (was DHCP; drifted .66.194 ~2026-05-22). No more drift. ERPNEXT_URL in secrets corrected .66.194 same day.
  • SSH: ssh biz-apps (alias in ~/.ssh/config)
  • Specs: 4 cores, 4GB RAM, 32GB disk, Debian 12, unprivileged, nesting=on (for Docker)
  • Stack: Docker + frappe_docker pwd.yml (10 containers: backend, frontend nginx, db MariaDB, redis ×2, queue ×2, scheduler, websocket, configurator)
  • Self-heal (2026-06-08): pwd.yml ships every service restart: on-failure, which does NOT recover after a clean stop/host reboot — that caused the 8-9 day silent outage (05-29→06-07). Fixed: infra/erpnext/compose.override.yml (forge repo, canonical) sets the 9 long-running services to restart: unless-stopped, deployed to /opt/frappe_docker/compose.override.yml on CT 104 with /opt/frappe_docker/.envCOMPOSE_FILE=pwd.yml:compose.override.yml so bare docker compose up -d now merges both. Validated by a full pct reboot 104: all 9 came back on their own. Recovery command is now cd /opt/frappe_docker && docker compose up -d (no -f needed).

Access

  • Public URL: https://erpnext.justinsforge.com (Cloudflare tunnel via media-server, behind Cloudflare Access)
  • CF Access policy: allow [email protected] + [email protected] only (app uid faefe1c2-b031-459c-8144-9e0f130e9f7e)
  • LAN URL (backend automation only, bypasses CF Access): http://192.168.86.194:8080 — must send Host: erpnext.justinsforge.com header so Frappe nginx routes to the frontend site
  • Site name: frontend (pwd.yml default; FRAPPE_SITE_NAME_HEADER=frontend in compose so all hostnames resolve to it)

Accounts

User Role Use
Administrator superuser Bench CLI, migrations, emergency. Admin API key still in secrets as fallback.
[email protected] System Manager Justin's daily UI login
[email protected] System Manager + Accounts Manager + Accounts User Live (created 2026-06-08). Dedicated API service user; Forge scripts now auth as this. System Manager alone does NOT grant the Account/Journal Entry/Invoice doctypes (those list Accounts Manager/User; Administrator only works because it's a superuser) — the accounting roles are required for any books work. Keys in secrets as ERPNEXT_BOT_API_KEY/_SECRET. Rotate via forge_erpnext_create_bot_user.py --rotate-keys; re-sync roles by re-running it.

Secrets

  • ~/.forge-secrets/erpnext.env (chmod 600):
  • ERPNEXT_PUBLIC_URL (https://erpnext.justinsforge.com)
  • ERPNEXT_URL (http://192.168.86.194:8080, LAN backend path)
  • ERPNEXT_SITE (frontend)
  • ERPNEXT_ADMIN_API_KEY, ERPNEXT_ADMIN_API_SECRET (Administrator token, fallback)
  • ERPNEXT_BOT_API_KEY, ERPNEXT_BOT_API_SECRET (forge-bot service user, preferred)

⚠️ MariaDB root password is admin (pwd.yml default). Acceptable only because DB is container-internal, never exposed. Do NOT bind-mount or port-forward MariaDB.

Companies (seeded 2026-05-03)

Legal name Abbr Role
Wiebelhaus Enterprises LLC WIEB S-corp parent, Group company for consolidation
JustinWiebVR LLC JWVR wholly-owned sub, parent=WIEB
Shop Nova Design LLC NOVA wholly-owned sub, parent=WIEB
Sip 'N' Serve Society LLC SIP wholly-owned sub, parent=WIEB
Gus Outdoor Co GOC standalone, 40% partnership owned by WIEB via equity method

Account 1850 - Investment in Gus Outdoor Co (40%) lives on WIEB's chart under 1800 - Investments. Equity-method posting will be a recurring journal entry from a Forge cron job once Gus's books are integrated.

All companies: USD, United States, Standard with Numbers COA, calendar fiscal year (Jan 1 → Dec 31).

Group COA inheritance (important): WIEB is a group company; JWVR/NOVA/SIP have parent_company=WIEB and inherit WIEB's chart of accounts. You CANNOT add an account directly to a child (Frappe rejects: "add to root level Company"); add it to WIEB (or GOC, standalone) and it propagates to the children. Any account-creation script must target only root companies.

Owner-equity accounts (seeded 2026-06-08, forge_erpnext_seed_equity_accounts.py): on all 5 companies under 3000 - Equity: 3600 Owner's Capital / Contributions (personally-paid business costs → equity contribution), 3700 Owner's Distributions / Draws (commingled personal spend reclassified out), 3800 Due to/from Owner (shareholder loan). Empty scaffolding; no postings yet (await reconciliation + CPA sign-off on treatment).

Sales-tax templates: deferred. Encode rate/jurisdiction/liability-account decisions = CPA territory; Shopify owns Nova's collection. Not built yet on purpose.

Forge tooling

  • forge/scripts/forge_erpnext_seed_companies.py — idempotent company seed; reusable for future entity adds. Sets User-Agent header (CF WAF blocks default Python UA). Now prefers bot creds (admin fallback).
  • forge/scripts/forge_erpnext_healthcheck.py — liveness check; hits LAN /api/method/ping, exit 0=up / 1=down. Wired into forge_canary.py as the erpnext check (daily 06:00) so a silent stack outage can't go unseen again (the 05-29 outage ran 8-9 days blind). Added 2026-06-08.
  • forge/scripts/forge_erpnext_create_bot_user.py — idempotent creator for the forge-bot service user; assigns System Manager + Accounts Manager + Accounts User, generates API keys; re-run heals roles, --rotate-keys reissues the secret. Added 2026-06-08.
  • forge/scripts/forge_erpnext_seed_equity_accounts.py — idempotent seeder for the 3 owner-equity accounts on all 5 companies (creates on root companies only; verifies children inherited). Added 2026-06-08.
  • forge/infra/erpnext/compose.override.yml — canonical self-heal override (unless-stopped); deployed to CT 104, wired via COMPOSE_FILE in the stack .env. Added 2026-06-08.
  • forge/scripts/forge_cloudflare_access_add.py — generic CF Access app + policy helper. First use was this LXC; reusable for future subdomains needing Access gating.

Future tooling (not yet built)

  • forge_books/ — custom Frappe app, will live in its own git repo, bind-mounted into the bench. Hosts Plaid bridge, equity-method posting cron, custom reports for the brand portfolio. Upgrade-safe layer — never edit core ERPNext.
  • forge_plaid_sync.py — nightly Plaid → ERPNext bank transaction sync.
  • Telegram coordinator-bot tool: surface batches of pending categorizations for confirm/correct flow.

Editing ERPNext safely

Use these layers (upgrade-proof → fork-pain): 1. Customize Form (UI) — add fields, layout 2. Client/Server Script (UI, lives in DB) — JS/Python on doctypes 3. Custom App (forge_books) — own doctypes, hooks, reports 4. Override via custom app — monkey-patch core methods 5. Fork ERPNext core — only if unavoidable, becomes a maintenance burden

Update cadence

  • Patch releases: weekly
  • Minor: every 1-3 months
  • Major (v15→v16): yearly
  • Plan: pull new images monthly, run bench migrate. Custom app stays on version-pinned hooks.

Useful API patterns

# From biz-apps (bypasses CF Access):
curl -H 'Authorization: token <key>:<secret>' \
     -H 'Host: erpnext.justinsforge.com' \
     'http://localhost:8080/api/resource/<Doctype>'
# From Console scripts: User-Agent must NOT be Python default (CF WAF bans it)
headers = {
    "Authorization": f"token {key}:{secret}",
    "User-Agent": "forge-erpnext-client/1.0",
}

Why ERPNext over Odoo

Decided 2026-05-03. Key drivers: - Cost: $0 vs $61-80/mo - Native Plaid + bank reconciliation built in (Odoo Community lacks both) - Free multi-company consolidation report - MIT license, no Enterprise tier — Frappe Technologies has explicitly committed to no open-core split - REST API is genuinely Claude-friendly — every doctype is an endpoint - Tradeoff: UI slightly older than Odoo (acceptable; we live in it daily, polish doesn't beat the feature deltas)

Full comparison was logged to memory/daily/2026-05-03.md.