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_URLin secrets corrected.66→.194same 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.ymlships every servicerestart: 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 torestart: unless-stopped, deployed to/opt/frappe_docker/compose.override.ymlon CT 104 with/opt/frappe_docker/.env→COMPOSE_FILE=pwd.yml:compose.override.ymlso baredocker compose up -dnow merges both. Validated by a fullpct reboot 104: all 9 came back on their own. Recovery command is nowcd /opt/frappe_docker && docker compose up -d(no-fneeded).
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 uidfaefe1c2-b031-459c-8144-9e0f130e9f7e) - LAN URL (backend automation only, bypasses CF Access): http://192.168.86.194:8080 — must send
Host: erpnext.justinsforge.comheader so Frappe nginx routes to thefrontendsite - Site name:
frontend(pwd.yml default;FRAPPE_SITE_NAME_HEADER=frontendin 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 intoforge_canary.pyas theerpnextcheck (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-keysreissues 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 viaCOMPOSE_FILEin 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.