-
Notifications
You must be signed in to change notification settings - Fork 0
Data Ownership and Export
The loudest question about any self-hosted ERP is the one nobody asks until year two: if I stop liking this, do I get my data back, and in what shape? This page answers it — what you can take out, in what format, who can take it, and what Forge sends anywhere without being told to. It closes with a topology fact that shapes the answer for anyone running more than one company: one install is one company.
The short version: the data is in your own PostgreSQL, on your own hardware; the whole database comes out of the admin UI in a documented open format; per-screen and per-report exports are ordinary CSV/XLSX/PDF; and there is no telemetry, analytics or licence-check client anywhere in the API.
Admin → Database (/admin/database) has two buttons: Export downloads the entire database as a zip, and Import loads such a zip back. Both sit behind the Admin role, and both are deliberately exempt from capability gating — this is the recovery tool an admin reaches for when an install is in a bad state, so it must never be switched off by the capability an admin is trying to fix. See Capability Gating for what "bootstrap-exempt" means.
The archive is not a proprietary blob. It is:
manifest.json schema, table, column list, row count, SHA-256, per table
tables/<schema>.<table>.copy PostgreSQL COPY text, one file per table
COPY text over a binary format is a deliberate choice: it is stable across PostgreSQL versions, diffable, and readable by anything that can read a tab-delimited file. Because the manifest records the column list per table, an import loads the intersection of dumped and current columns — dropped columns fall away, new ones take their defaults — so a dump survives modest schema drift between the install it came from and the install it lands in.
The same archive layout is the on-disk layout of the forge-db CLI's dump verb, zipped, which makes the two interchangeable in both directions: unzip a UI export and feed it to forge-db import --from, or zip a CLI dump and upload it through the browser. The CLI remains the path for a cross-database rebuild — dump the old database, provision a fresh one from the schema tree, import the data back — because an in-app import necessarily targets the install it is running in. The workflow is specified in docs/DESIGN.md §6.2 in forge-db.
What the export omits is infrastructure that re-creates itself: the Hangfire job store, the schema-harness bookkeeping schema, and EF's history table. Everything your business put in is there, including soft-deleted rows — Forge marks records deleted rather than removing them, so the history really does come along.
Import is the other half of the same tool, and it is destructive by design: it truncates the selected tables and reloads them in one transaction, so a failure leaves the target as it was. Three filters let you leave the garbage behind — exclude globs for whole tables you do not want (event logs, dead features), an optional purge of soft-deleted rows, and a post-load foreign-key pass that re-validates every constraint and reports orphans rather than silently accepting them. Orphans are the thing this workflow exists to surface. The UI double-confirms before it runs.
Integration credentials are sealed with ASP.NET Data Protection, and the Data Protection keys live in the database — which means they are in the archive alongside the ciphertext they unseal. That is what makes an archive self-sufficient: restore it and your integrations still work. It also means a database export is as sensitive as your .env. Store it accordingly, and prefer a rotation over reuse if one ever leaves your control. Hardening a Production Install covers the secrets that are not in it.
The dump is a convenience, not the only door. Forge runs an ordinary PostgreSQL instance you control, so psql, pg_dump, a BI tool's native Postgres connector or an ETL job all work with no cooperation from Forge and no involvement from anyone else. Nothing about the data model is hidden: the complete desired-state schema is one-object-per-file SQL in the public forge-db repo, and docs/functional-reference/database-schema.md is the narrative reference for its conventions — snake_case throughout, timestamptz in UTC, integer id primary keys, {table}_id foreign keys, deleted_at soft deletes. You can read the whole schema before you install anything.
One thing that is not in the database: uploaded files. Drawings, documents, photos and signed PDFs live in object storage — MinIO or any S3-compatible service, or a plain local directory — so a full copy of an install is the database plus the buckets. That pairing is exactly what the backup sidecar captures, and Backup and Restore is the page for it.
You should not have to dump a database to get a spreadsheet.
| Surface | Formats | Who |
|---|---|---|
| Any list screen built on the shared data table | CSV, XLSX | Anyone who can see the list |
| Saved reports (report builder) | CSV, XLSX, PDF | Admin, Manager, Office Manager, PM |
| GL trial balance, GL detail, per-account journal summary | CSV | Controller role, GL view capability |
List exports are a built-in pair of toolbar buttons on the shared data-table component, so they appear on most list screens across the app without each page implementing anything. The honest caveat: an export writes the rows the grid is currently holding, with the columns currently visible — it respects your filters and your column choices, which is usually what you want, but on a server-paginated list it is not a guarantee of every row on the server. Use the report builder or the database export when you need completeness.
Report exports run a saved report definition and render it. The report builder speaks a fixed set of entity sources covering the core transactional records — jobs, parts, customers, vendors, quotes, sales and purchase orders and their lines, invoices, shipments, payments, time entries, expenses, leads, assets, production runs, lot records, QC inspections, maintenance and downtime logs, returns, bin movements — with related fields such as customer name joined in. Exports are capped at a fixed row ceiling, so a very large report truncates rather than running forever; docs/functional-reference/reports.md carries the current limit and the per-format detail.
Accounting exports deserve a specific mention because they are the ones an outside accountant asks for. Trial balance, full GL detail (one row per journal line) and a per-account period-net journal summary all download as CSV — the last being the "one monthly journal entry" a CPA keys into their own system, aggregated exactly the way the optional QuickBooks push aggregates it. They sit behind the read-only GL view capability rather than the full-GL posting capability, so they remain available after a ledger has been deactivated: switching the general ledger off does not lock you out of the books it produced. See Accounting Modes.
Everything the UI reads, the API can serve. It is a conventional JSON API under a stable api/v1 prefix with paginated list endpoints, and for a headless caller there are user-bound system API keys — a key that authenticates as a real user, so role checks and audit rows attribute to that identity and deactivating the user kills the key. That is the practical automation path today, and API Access has the token flow, the error contract and the key mechanics.
Be aware of what is and is not built on the dedicated BI lane. A separate BI API key scheme ships and works: unbound keys, a synthetic read-only BI client role, optional IP allow-listing, scoped to the /api/v1/bi/* route prefix and gated on its own capability. The routes behind it are not built yet — the surface currently exposes an identity probe (whoami) and nothing else, with entity exports and scheduled feeds named in the code as future work. Until those land, a BI tool should point at PostgreSQL directly or at the regular API with a system key. Outbound webhooks are in the same half-built state — subscriptions, signing secrets and delivery history all exist, but no dispatcher delivers them, so do not architect an export pipeline on Forge pushing events to you. Limits and Non-Goals keeps the running list of what is scaffolding and what is finished.
There is no telemetry client, no analytics SDK, no usage beacon and no licence check anywhere in the API. No code path contacts Armory Works at runtime, and there is no code path that could be switched on to make it. The frontend ships no third-party analytics or tag manager and pulls no fonts or scripts from a CDN.
Every outbound connection an install makes is one an operator configured:
- Your accounting provider, carriers, address validation, SMTP, e-commerce storefronts, cloud-storage and calendar destinations, e-signature and telephony — configured per integration, in your admin settings, and not contacted at all when left unconfigured. Configuration and Integrations is the inventory, including which of them are real connectors and which are still placeholders returning fabricated data.
- Sign-in providers, if you enable SSO — Google, Microsoft or your own OIDC endpoint.
- The AI assistant, which runs against a self-hosted Ollama in your own stack. It is profile-gated: leave it off and the container does not run.
- Crash reporting, which is opt-in twice over. It only reports when an operator sets a crash-reporting DSN and the device's diagnostics toggle is on, and the DSN is meant to point at the optional self-hosted crash sidecar in the compose stack. The code says it plainly: it never points at Armory Works.
The one place your box does talk to us is at deploy time, pulling container images from GHCR — a package registry fetch, on your schedule, containing nothing about your data. An install that has already pulled its images runs indefinitely with no outbound internet at all, which is a legitimate deployment posture for a shop that wants one.
There is no tenant dimension in the data model. No entity carries a TenantId; there is no tenant filter, no per-tenant schema, and no row-level partition by company. One Forge install serves one legal entity. Plan the topology around that before data entry starts, because it is not a setting you can flip later.
What you do get inside a single install:
- Multiple sites. Company locations are first-class records — address, contact, a default flag — and each can override the install's working calendar, so a multi-site shop observes local holidays at each plant without separate configuration.
- Multiple currencies. Currencies are configured records with one flagged as the base; the ledger's book carries its own functional currency and reporting time zone.
- A ledger that is single-entity now but shaped for more. Every GL row carries a book identifier and the posting engine enforces book consistency, which is groundwork rather than a feature: the rest of the model — parts, jobs, orders, inventory — has no equivalent axis, so a second book does not give you a second company.
Two legal entities means two stacks: two databases, two API containers, two URLs. That is a supported and unremarkable thing to run — Installation and the forge-deploy README cover co-hosting several installs on one box as well as splitting one install across several — but it is two of everything, including two sets of users, two capability configurations and two upgrade cycles. Consolidated reporting across them is your problem to solve, most plausibly by pointing a BI tool at both databases. See Limits and Non-Goals.
The exit is deliberately dull, which is the point:
- Export the database from the admin UI, or
pg_dumpit yourself. - Mirror the object-storage buckets — Backup and Restore has the commands.
- Read the schema in forge-db to understand what you are holding.
- Pull whatever shaped extracts you need from the report builder, the list exports and the accounting exports.
- Stop the stack.
No key needs to be surrendered, no service needs to be told, and nothing stops working because a subscription lapsed. Under Apache 2.0 you may also keep running the version you have, indefinitely, with no further involvement from anyone — see FAQ.
Forge · Apache 2.0 · built by Armory Works — this wiki maps the docs; the authoritative detail lives in docs/.
Evaluating
Running it
- Installation
- First Week
- Configuration and Integrations
- Hardening a Production Install
- Backup and Restore
- Upgrades and Rollback
- Accounting Modes
Using it
- App Surfaces
- Shop Floor Kiosk
- Mobile and Offline
- Access and Roles
- Customizing an Install
- Feature Reference
- UI Flows
Building on it
- API Access
- Architecture
- Capability Gating
- Workflow, Gates and Approvals
- Data Ownership and Export
- Glossary
Contributing
Repo wikis