Full data export
You can export your org’s data on demand from the admin UI: every repository as a git bundle, the issues, pull requests, comments, labels, milestones and releases of each repository, your package inventory, your scan findings and package-policy decisions, your build and release attestations with their signatures, the SBOMs of your repositories and container images, and the audit trail of the last 90 days. Everything is in a standard format, downloadable as a single .tar.gz with a signed SHA-256 manifest covering every file.
This page documents what’s in the bundle, how to verify it, and what limits apply. The export is built into the €30/seat plan; no Enterprise tier required.
What’s in the bundle
The bundle’s layout, every file in it and the shape of each record are documented on one page: Export schema. In short: a git bundle per repository, the issues, pull requests, comments, labels, milestones, releases and settings of each repository as NDJSON, your package inventory and file list, your third-party component inventory, your registry access log, your scan findings, package-policy decisions and signed attestations, the SBOMs of your repositories and container images, and your audit trail, all under one DSSE-signed SHA-256 manifest. Package contents come on request as a second archive, see §“Package contents”.
Package contents
Tick Include package contents when you start an export (or send {"include_packages": true} to the API) and the export produces a second archive next to the bundle: every file of every package version your org owns, read from the registry’s storage at export time.
export-<your-org-slug>-<timestamp>.packages.tar
└── packages/
├── npm/<name>/<version>/<file> — e.g. npm/@acme/lib/1.2.0/lib-1.2.0.tgz
├── npm/<name>/<version>/<file>.cdx.json — the file's SBOM (CycloneDX), and .spdx.json (SPDX)
├── maven/<group>/<artifact>/<version>/<file>
├── pypi/<name>/<version>/<file>
├── generic/<name>/<version>/<file> — and the same shape for every other non-container type
└── container/<image>/ — one OCI image layout per image
├── oci-layout
├── index.json — one entry per tag
└── blobs/sha256/<digest> — manifests, configs and layers- Signed with the bundle. The bundle’s
manifest.jsonrecords the archive’s name, size and SHA-256 (packages_archive) and every file inside it (objects), so the one DSSE signature covers both downloads.VERIFY.mdin the bundle includes the commands; see also §“Verifying the bundle”. - SBOMs. Each package file comes with the software bill of materials fremforge made for it when it was uploaded:
<file>.cdx.json(CycloneDX) and, where one was made,<file>.spdx.json(SPDX), next to the file, for every non-container type. They are in the manifest’sobjectslike every other file. A file the scanner could not process has none. If a package itself ships a file with exactly that name, the package’s file is kept and the SBOM is left out. Container images are not given per-file SBOMs here; their SBOMs are described in SBOMs. - Containers are OCI image layouts, which
skopeo,crane,orasandpodmanread directly. For example, to push a tag to another registry:skopeo copy oci:packages/container/<image>:<tag> docker://<registry>/<image>:<tag>. - Plain
.tar, not compressed: package files (.tgz, wheels, jars, image layers) are already compressed. - Download link: shown as Package contents next to the bundle’s link, and returned by the API as
packages_signed_url. Valid for 7 days, like the bundle’s. The archive itself is deleted after 7 days; start a new export if you need it again. - 50 GiB limit. If your org’s package contents exceed 50 GiB, the request is refused straight away (
export-too-large) and the export without package contents is still available. Contact support@frem.sh for an assisted export of larger registries.
Every file is checked against the inventory’s size and SHA-256 as it is written, and every SBOM against the size and SHA-256 recorded when it was made; an export whose package files do not match fails rather than delivering an archive that disagrees with its manifest.
Scan findings, policy, attestations and SBOMs
Every export also carries the security data fremforge holds about your org, covered by the signed manifest like every other file:
findings/: the findings of every scanner, one NDJSON file each (static analysis, dependencies, licences, container images, secrets, malware in uploads, dependencies and container layers, registry-package vulnerabilities and licences, unpinned workflow actions), with their dismissals: who dismissed a finding, why and until when.policy/: your package policy, its allow and deny rules and approvers, the current verdict for each package file, gate decisions (including the ones shadow mode would have blocked), exceptions with their approvals, malware overrides, and each scanner’s org settings.attestations/: your build provenance, release attestations and attestations uploaded with an SBOM, each with its DSSE envelope and, where one was made, its keyless Sigstore bundle.sboms/: every SBOM fremforge stores for your repositories (sboms/repos/, CycloneDX and, where one was made, SPDX) and your container images (sboms/images/), as the documents themselves, indexed bysboms/index.ndjson. Each index line says whether the document matched the SHA-256 recorded when it was stored. Package SBOMs are not here: they come next to their files in the package contents archive.
The files and their fields are listed in Export schema.
What’s NOT in the bundle (and why)
- Package contents and container image layers, unless you ask for them: they then come as a separate archive, see §“Package contents”.
- LFS file content, see §“LFS content”.
- Release assets,
releases.ndjsonholds each release’s metadata, including the download URL of every asset; fetch the assets from those URLs. - Forgejo Actions logs and run artifacts, they are not exported. Download a run’s logs and artifacts from the run page or the Actions API while you need them.
- Package SBOMs, unless you ask for package contents: each registry package file’s SBOM travels next to the file in the package contents archive. Repository and container-image SBOMs are in the bundle, under
sboms/(see §“Scan findings, policy, attestations and SBOMs”). - Invoices and billing records, not part of the bundle; they are in the admin UI under Billing.
- Other tenants’ data, the audit-log slice contains only your org’s own audit events.
How to start an export
- Org admin UI → Data export tab.
- Optionally tick Include package contents (see §“Package contents”).
- Click Start a new export.
- The export runner picks the job up within 5 minutes. A small org is ready a few minutes later; time grows with the size of your repositories. The Data export tab shows the job’s status and, once it is ready, the download link.
- The download link is valid for 7 days after the bundle is ready, and the bundle file is deleted 7 days after it was made, including its disaster-recovery copy. Under a legal hold the file is kept.
If an export fails, the person who requested it gets an email with the reason, and you can start a new one right away.
Rate limit
One export per org per 24 hours. Hard cap. A failed export does not count, so you can retry at once. The reason: git bundle-ing a multi-GB repo and computing manifest hashes isn’t free, and a misclick shouldn’t spawn a hundred exports overnight.
If you genuinely need a second export inside 24 hours, contact support@frem.sh. We can lift the cap on request.
Size limits
The archive is streamed to storage as it is built, so its size is not limited by memory or by a single-upload cap. What does bound an export today:
- About 10 GB of repository bundles and metadata, the working space the export runner stages files in before archiving them.
- One hour for the whole export.
- 50 GiB of package contents, when you ask for them; refused at request time, see §“Package contents”.
An org above either limit fails with an error rather than producing a partial bundle. Contact support@frem.sh and we run an assisted export instead (typically a direct sync into your own bucket).
For most orgs, repository content is by far the largest part; metadata and audit data are small in comparison.
Verifying the bundle
Checking that every file reached you intact takes one sha256sum run. Prerequisite: jq (the SHA-256 utility ships with every modern OS, sha256sum on Linux, shasum -a 256 on macOS).
Unpack the archive, then, inside the unpacked directory:
# Linux
sha256sum --check <(jq -r '.files[] | "\(.sha256) \(.path)"' manifest.json)
# macOS
shasum -a 256 -c <(jq -r '.files[] | "\(.sha256) \(.path)"' manifest.json)If every line reports OK, every file in the bundle matches the SHA-256 hash fremforge recorded. A truncated or corrupted download does not unpack cleanly, or fails this check.
What this catches and what it doesn’t
The SHA-256 check catches post-export tampering (download corruption, MITM modification of an unsigned mirror) and matches the bundle to the manifest fremforge produced. To prove the manifest itself was produced by fremforge (not a third party who swapped your bundle for theirs), run the signed-manifest verification in the next section, it cryptographically binds the manifest bytes to fremforge’s published Ed25519 builder key.
Verifying the signed manifest
The bundle ships with a DSSE-signed in-toto Statement over manifest.json at manifest.json.intoto.jsonl. The signature is produced with fremforge’s SLSA builder Ed25519 key, the same key that signs SLSA build provenance for artifacts produced by the hosted runner, and verifies against the public trust root at https://www.frem.sh/.well-known/slsa-trust-root.json. The active key is fremforge-slsa-builder-v2, in use since the key rollover of 2026-09-12; the commands below select it. VERIFY.md inside your bundle carries the same commands with the key that signed that bundle filled in.
Use the key the envelope names. The
keyidinmanifest.json.intoto.jsonl(jq -r '.signatures[0].keyid' manifest.json.intoto.jsonl) is the key that signed it. A bundle exported before 2026-09-12 is signed withfremforge-slsa-builder-v1, which stays in the trust root for verifying old signatures (it no longer signs anything); for such a bundle, selectfremforge-slsa-builder-v1in step 1. A current bundle does not verify under v1: step 2 then printsDSSE signature verification FAILED.
You verify with curl, jq, openssl, and python3, all distribution-default. No fremforge CLI. No Sigstore Fulcio / Rekor / TUF-CDN dependency.
1. Fetch the public trust root and extract the pubkey
curl -sSfL https://www.frem.sh/.well-known/slsa-trust-root.json -o trust-root.json
jq -r '.trusted_keys[] | select(.kid=="fremforge-slsa-builder-v2") | .pem' \
trust-root.json > builder.pub2. Verify the DSSE envelope signature
python3 - <<'PY'
import base64, json, hashlib, subprocess, sys, pathlib
env = json.loads(pathlib.Path("manifest.json.intoto.jsonl").read_text().strip())
payload_type = env["payloadType"]
payload_b64 = env["payload"]
payload = base64.b64decode(payload_b64)
# Reconstruct the DSSE pre-authentication encoding.
pae = (
b"DSSEv1 "
+ str(len(payload_type)).encode() + b" "
+ payload_type.encode() + b" "
+ str(len(payload)).encode() + b" "
+ payload
)
pathlib.Path("pae.bin").write_bytes(pae)
sig_b64 = env["signatures"][0]["sig"]
pathlib.Path("sig.bin").write_bytes(base64.b64decode(sig_b64))
# Ed25519 verify with openssl: pkeyutl -verify needs the SAME bytes
# the signer hashed. EdDSA is "pure" — no pre-hash — so feed the
# raw PAE buffer.
r = subprocess.run(
["openssl", "pkeyutl", "-verify", "-pubin",
"-inkey", "builder.pub",
"-rawin", "-in", "pae.bin",
"-sigfile", "sig.bin"],
capture_output=True, text=True,
)
if r.returncode != 0 or "Signature Verified Successfully" not in r.stdout:
print("DSSE signature verification FAILED:", r.stdout, r.stderr, file=sys.stderr)
sys.exit(1)
print("DSSE signature: OK")
# Cross-check the signed manifestSha256 matches the on-disk manifest.json.
stmt = json.loads(payload)
claimed = stmt["predicate"]["manifestSha256"]
actual = hashlib.sha256(pathlib.Path("manifest.json").read_bytes()).hexdigest()
if claimed != actual:
print(f"manifestSha256 mismatch: signed={claimed} actual={actual}", file=sys.stderr)
sys.exit(1)
print(f"manifestSha256: OK ({actual})")
# Sanity-check the predicate type.
if stmt["predicateType"] != "https://fremforge.eu/spec/export-bundle/v1":
print(f"unexpected predicateType: {stmt['predicateType']}", file=sys.stderr)
sys.exit(1)
print("predicateType: OK")
PY3. Verify every file’s SHA-256 against the (now-trusted) manifest
# Linux:
sha256sum --check <(jq -r '.files[] | "\(.sha256) \(.path)"' manifest.json)
# macOS:
shasum -a 256 -c <(jq -r '.files[] | "\(.sha256) \(.path)"' manifest.json)If all three steps pass, the bundle is end-to-end tamper-evident: every file matches the SHA-256 fremforge recorded, the manifest matches what fremforge signed, and the signature was produced by the published builder key.
4. Package contents (only if you asked for them)
Download the .packages.tar into the same directory, then check the archive and every file in it against the same signed manifest:
echo "$(jq -r '.packages_archive.sha256' manifest.json) $(jq -r '.packages_archive.name' manifest.json)" | sha256sum --check
tar -xf "$(jq -r '.packages_archive.name' manifest.json)"
sha256sum --check <(jq -r '.objects[] | "\(.sha256) \(.path)"' manifest.json)What the signature proves
- The bundle bytes you received are byte-identical to what fremforge produced at export time.
- The manifest was produced by fremforge’s builder key, not by a third party.
- The signed predicate (
https://fremforge.eu/spec/export-bundle/v1) declares the org slug, export request id, file count, and total bytes, substitution of a different tenant’s bundle would not verify.
What it doesn’t prove
- Timeliness, the signed
exportedAtis fremforge-asserted; for third-party-witnessable timestamps, cross-reference the bundle’s audit-log slice against the public WORM-anchored audit chain. - Key revocation, the trust root JSON is the source of truth; if fremforge later revokes the builder key, old signatures stop verifying when the key disappears from
trusted_keys. Re-fetch the trust root if you’re verifying long after the export was issued.
Re-clone a repo from a bundle
Each bundles/<repo>.bundle is a standard git bundle. To restore:
git clone bundles/<repo>.bundle <repo>
cd <repo>
git remote remove origin # bundle URL is local
git remote add origin <new-fremforge-or-elsewhere-url>
git push -u --mirror origin--mirror preserves all branches + tags + refs. The bundle reconstructs the full repo history, identical SHAs, no force-push required to any new remote.
LFS content
LFS file content is not in the bundle. The git bundle carries each LFS file’s pointer (its oid and size), so the history is complete; the content itself stays on fremforge’s LFS server. Pulling many gigabytes of binary attachments into every export would balloon its size for everyone.
metadata/<repo>/lfs-manifest.ndjson is the per-repository LFS inventory: one line per LFS object that any branch, tag or pull request in the repository references.
| Field | Meaning |
|---|---|
oid | the object’s SHA-256, as in its LFS pointer |
size | size in bytes |
stored | true when fremforge’s LFS server holds the object; false for a pointer that was committed without its content ever being uploaded |
path | a path at which the repository references the object (one path per object, even if it is committed at several) |
download_path | path on frem.sh that serves the object’s bytes (HTTP Basic auth, username token, an API token as the password); null when stored is false |
The inventory is built from the repository’s history, so objects uploaded to the LFS server but referenced by no branch, tag or pull request are not listed. A repository without LFS gets an empty file. If the repository’s history references LFS objects but fremforge’s LFS server can’t be queried, the export fails rather than shipping an incomplete inventory.
To download LFS content, after re-cloning the repo from the bundle:
git lfs install
git lfs fetch --allThis pulls from fremforge’s LFS server while you’re still a paying customer (LFS bandwidth is included). After cancellation we keep your data for 90 days; LFS pulls work during that window.
For an enterprise export that includes LFS bytes inline, contact support. We’ll size the bundle and may need to run the export as an OBS-to-OBS sync rather than a downloadable archive.
Switching to another provider (EU Data Act)
This section is the information notice required by Art. 26 of the EU Data Act (Regulation (EU) 2023/2854). The contractual terms behind it are ToS §16.6.
You can switch to another provider, port everything to your own infrastructure, or have it all erased — at any time, with or without cancelling.
| How to start | Admin UI → Data export, or email support@frem.sh. No form, no retention call. |
| Notice period | Two months maximum under the Data Act. We start on receipt — you do not wait. |
| Transition period | 30 calendar days maximum. Extendable only where genuinely technically unfeasible, and only with a written justification to you inside 14 working days. |
| During transition | Service continues at the same service levels and the same price. Nothing degrades because you are leaving. |
| Retrieval window | 30 days after transition ends, or the 90-day post-cancellation window above — whichever is longer. |
| Cost | Nothing. No switching charge, no egress fee, no early-termination penalty. The Data Act allows cost-based charges until 12 January 2027; we do not levy them. |
Formats. Everything in the bundle is a standard open format — git bundle, JSONL, in-toto DSSE, JSON — documented on the Export schema page. No fremforge-proprietary format, and no fremforge tooling needed to read any of it.
Importing into a self-hosted Forgejo. fremforge runs a modified Forgejo build, but the patch series does not alter Forgejo’s data model, so an export imports directly into a stock self-hosted Forgejo instance. Re-clone each repo from its bundle (§“Re-clone a repo from a bundle”) and push to the new host with --mirror. Issues and pull requests move via the GitHub-API-compatible endpoints noted below.
Erasure instead. If you want the data gone rather than moved, say so in the same request. We erase all exportable data and digital assets per DPA §9 and confirm in writing.
Help is part of the deal. Under ToS §16.6(h) we assist the switch in good faith — including answering technical questions from the provider you are moving to. Ask.
Cancellation and the export
If you cancel your fremforge subscription:
- Your last 7-day export window stays valid. The signed URL keeps working.
- A new export can be triggered any time during the 90-day post-cancellation read-only retention window. Same flow; same UI; same bundle.
- An export queued late in the 90-day window, even at +89d 23h, is honoured: fremforge holds physical deletion of your Forgejo org for up to 7 days past the 90-day mark whenever an export is queued or running. After the export completes, deletion proceeds on the next scheduled sweep.
- After 90 days (plus the export-grace window above), the data is purged per DPA §9; export availability ends with it.
If you’re cancelling specifically because you want a final export, do the export first, verify it locally, then cancel. The window is generous, but verifying-then-deleting is the rigorous flow.
Just one repository?
The full export is org-scoped, every repo with its issues, pull requests and releases, the package inventory and the audit slice, in one signed bundle. If you only need a single repository, the simpler paths are:
git clone --mirror, full Git history, branches, and tags as an offline-clonable bare repo. fremforge does not publicly expose port 22; use either the SSH-over-443 endpoint (ssh.frem.sh:443) or the HTTPS form:# SSH over port 443 (single endpoint for all orgs; uses your registered SSH key): git clone --mirror ssh://git@ssh.frem.sh:443/<org>/<repo>.git # OR HTTPS clone with a PAT (`ffp_…`) from Settings → API tokens: git clone --mirror "https://x-access-token:${FREMFORGE_PAT}@frem.sh/<org>/<repo>.git" cd <repo>.git git lfs fetch --all # if the repo uses LFSHost-key fingerprints for
ssh.frem.share published atwww.frem.sh/ssh-fingerprints, verify on first connection. The result is agit bundle-ready directory;tar -czfit for a portable single file.Repo download as zip/tarball,
frem.sh/<org>/<repo>/archive/<branch>.zipor.tar.gzfor a snapshot of a single ref. No history, no LFS, just the working tree at that branch.Per-repo issues + PRs JSON, via the API,
GET /api/v1/repos/<org>/<repo>/issues?state=all&type=issuesand?type=pulls. The migration guides show the call shape; the response is GitHub-API-compatible JSON.
The org-wide signed export is the right tool when you want everything (every repository, its metadata, the audit log slice, the signed manifest); the per-repo paths above are right for the everyday “I need to send this one repo to someone” case.
Programmatic API
The same export is reachable via the REST API:
# Start a new export
curl -X POST \
-H "Authorization: Bearer <your-PAT>" \
https://frem.sh/_app/api/v1/orgs/<your-org>/exports
# ... or with package contents (separate archive, 50 GiB limit)
curl -X POST \
-H "Authorization: Bearer <your-PAT>" -H "Content-Type: application/json" \
-d '{"include_packages": true}' \
https://frem.sh/_app/api/v1/orgs/<your-org>/exports
# Poll for status
curl -H "Authorization: Bearer <your-PAT>" \
https://frem.sh/_app/api/v1/orgs/<your-org>/exports/<job-id>
# When status=ready, follow the signed_url field
# (and packages_signed_url, if you asked for package contents)OpenAPI spec: docs.frem.sh/reference/api/.
Privacy guarantees
The export is auditable both ways:
- Who exported: every export request is recorded in your own audit log (
data-export.requestedfrom the admin UI,data-export.queue.apifrom the API) with the requesting user or token. Actions fremverk support takes on your org are tagged as such in the same log. - Whose data: the audit-log slice is selected by your org’s tenant id, so it contains only your org’s own audit events.
- Signed URL hygiene: the download URL is a pre-signed, read-only link to your one archive, valid for 7 days. After that the URL stops working and the export disappears from the Data export tab.