Organization Import / Export
This document describes both export/import formats for organization data: the JSON envelope (full fidelity, for programmatic use) and the CSV ZIP (human-readable spreadsheets, for manual editing and bulk data operations).
Overview
Both formats capture the full state of an organization:
| Included | Description |
|---|---|
| Organization settings | License dates, flags, schema version, default activity attributes |
| Locales | One row/entry per ROPA locale (locale code, display name, default flag) |
| Partners | All partner organizations with addresses and contacts |
| Contracts | All contracts with linked partner and activity IDs |
| ROPA data | All processing activities, one document per locale |
| Templates | Handlebars templates (JSON only — not suitable for CSV) |
Import always replaces the organization if shortName already exists (delete + recreate, returns isError: false). If the org does not exist, it is created fresh.
Routes
All routes require the x-admin-secret HTTP header matching the ADMIN_SECRET environment variable.
JSON Export
GET /api/admin/org/export?shortName=<shortName>
Returns <shortName>-export.json.
curl -H "x-admin-secret: $ADMIN_SECRET" \
"https://your-host/api/admin/org/export?shortName=acme" \
-o acme-export.json
JSON Import
POST /api/admin/org/import
Content-Type: application/json
Body: the JSON file produced by the export route.
curl -X POST \
-H "x-admin-secret: $ADMIN_SECRET" \
-H "Content-Type: application/json" \
-d @acme-export.json \
"https://your-host/api/admin/org/import"
CSV ZIP Export
GET /api/admin/org/export?shortName=<shortName>&format=csv
Returns <shortName>-export.zip.
curl -H "x-admin-secret: $ADMIN_SECRET" \
"https://your-host/api/admin/org/export?shortName=acme&format=csv" \
-o acme-export.zip
CSV ZIP Import
POST /api/admin/org/import
Content-Type: multipart/form-data
Body: multipart field file containing the ZIP file.
curl -X POST \
-H "x-admin-secret: $ADMIN_SECRET" \
-F "file=@acme-export.zip" \
"https://your-host/api/admin/org/import"
Response codes (both formats)
| Status | Reason |
|---|---|
200 | Success — JSON body with { ok, shortName, orgId, message } |
401 | Missing or invalid x-admin-secret |
400 | Invalid body / bad ZIP archive |
422 | Validation failed |
JSON Envelope Format
Structure
{
"exportVersion": 1,
"exportedAt": "2026-04-05T10:00:00.000Z",
"organization": {
"shortName": "acme",
"clerkOrganizationId": "org_abc123",
"licenseStart": 1700000000000,
"licenseEnd": 1731556952000,
"licenseCost": 0,
"isBlocked": false,
"isPublic": true,
"isDemo": false,
"highestOuId": 4,
"highestActivityId": 6,
"highestPartnerId": 2,
"highestContractId": 1,
"schemaVersion": 13,
"defaultActivityAttributes": {},
"ropas": [
{ "locale": "en", "longName": "ACME Corp ROPA", "isDefault": true }
],
"partners": [ ... ],
"contracts": [ ... ],
"templates": [
{ "activityId": 0, "type": "ropaFrontPage" }
]
},
"ropas": [
{
"orgShortName": "acme",
"locale": "en",
"ous": [
{
"ouId": 1,
"ouName": "Marketing",
"ouColor": "#ff6600",
"activities": [ ... ]
}
]
}
],
"templates": [
{
"orgShortName": "acme",
"locale": "en",
"activityId": 0,
"type": "ropaFrontPage",
"description": "Front page template",
"handlebars": "<h1>{{organizationName}}</h1>..."
}
]
}
Serialization rules
- All MongoDB
_id/__vfields are stripped. ropaIdreferences insideorganization.ropas[]are stripped — resolved at import time by locale key.templateIdreferences insideorganization.templates[]are stripped — rebuilt at import.orgIdis stripped from each template — linked at import viaorgShortName.organizationLogoBuffers are base64-encoded strings in the export; decoded back to Buffers on import.- Embedded contract subdocument
_idfields are stripped.
CSV ZIP Format
File inventory
A ZIP produced by the CSV export contains the following files, all prefixed with {shortName}-:
| File | Description |
|---|---|
{shortName}-organization.csv | One header row + one data row for org scalar fields |
{shortName}-locales.csv | One row per locale |
{shortName}-partners.csv | One row per partner organization |
{shortName}-contracts.csv | One row per contract |
{shortName}-ropa-{locale}.csv | One file per locale; one row per activity |
{shortName}-templates.json | Handlebars templates (only present if templates exist) |
All CSV files use UTF-8 encoding, LF line endings, and RFC-4180 quoting (fields containing commas, double-quotes, or newlines are wrapped in double-quotes; internal double-quotes are escaped as "").
Multi-valued fields within a single cell are pipe-separated (|).
organization.csv columns
| Column | Type | Notes |
|---|---|---|
shortName | string | Must match ZIP filename prefix |
clerkOrganizationId | string | Clerk org ID |
licenseStart | number | Unix timestamp (ms) |
licenseEnd | number | Unix timestamp (ms) |
licenseCost | number | |
isBlocked | boolean | true / false |
isPublic | boolean | |
isDemo | boolean | |
highestOuId | number | Monotonic counter used to prevent ID reuse |
highestActivityId | number | |
highestPartnerId | number | |
highestContractId | number | |
schemaVersion | number | |
defaultActivityAttributes | JSON string | Serialized object |
locales.csv columns
| Column | Type | Notes |
|---|---|---|
locale | string | BCP-47 language tag (e.g. en, fr) |
longName | string | Display name for the ROPA document |
isDefault | boolean | Exactly one row must be true |
partners.csv columns
| Column | Type | Notes |
|---|---|---|
organizationId | number | 0 = the org itself (self-row, always required) |
organizationName | string | Short name |
organizationNameLong | string | Full legal name |
organizationColor | string | Hex color or empty |
organizationWebsite | string | URL or empty |
addressLine1 | string | Flattened from organizationPostalAddress |
addressLine2 | string | |
city | string | |
stateProvince | string | |
postalCode | string | |
country | string | |
organizationLogo | string | Base64-encoded image or empty |
organizationNotes | string | |
organizationContacts | JSON string | Array of contact objects |
contractOrder | pipe-sep numbers | Ordered contract IDs for this partner |
contracts.csv columns
| Column | Type | Notes |
|---|---|---|
contractId | number | Must be ≤ highestContractId |
contractName | string | |
contractUrl | string | |
contractExpirationDate | string | ISO date string or empty |
contractDescription | string | |
activityIds | pipe-sep numbers | Activity IDs covered by this contract |
partnerIds | pipe-sep numbers | Partner IDs party to this contract |
ropa-{locale}.csv columns
One file per locale. Each row represents one activity nested inside an OU.
| Column | Type | Notes |
|---|---|---|
locale | string | Must match the filename locale |
ouId | number | Must be ≤ highestOuId |
ouName | string | |
ouColor | string | |
activityId | number | Must be ≤ highestActivityId |
activityName | string | |
purposeShort | string | |
purposeLong | string | |
legalbasis | pipe-sep | Array field |
legalbasisLong | string | |
legalbasisSpecial | pipe-sep | Array field |
dataCategories | pipe-sep | Array field |
datasubjectCategories | string | |
activityCategories | pipe-sep | Array field |
dataOrigin | string | |
timeLimit | string | |
profiling | boolean | |
communications | string | |
communicationsLong | string | |
controllers | pipe-sep | Array field |
processors | pipe-sep | Array field |
transfers | boolean | |
transfersLong | string | |
securityLevel | string | |
securityMeasuresLong | string | |
active | boolean | |
timestamp | number | Unix timestamp (ms) |
templates.json
Same structure as the templates[] array in the JSON envelope. Only present in the ZIP when the organization has at least one template. Not converted to CSV because Handlebars source contains HTML markup that is not spreadsheet-friendly.
ZIP Integrity Checks
Before import, the client-side zipCheck function validates:
*-organization.csvis present and has exactly one data rowshortNamein the CSV matches the filename prefix*-locales.csv,*-partners.csv,*-contracts.csvare present- One
*-ropa-{locale}.csvexists for every locale listed in locales.csv - No unexpected files appear in the ZIP
- All locales in ropa filenames are listed in locales.csv
locales.csvhas exactly oneisDefault=truerowpartners.csvincludes a self-org row (organizationId=0)- All
organizationIdvalues in partners.csv are ≤highestPartnerId - All
contractIdvalues in contracts.csv are ≤highestContractId - All
partnerIdsin contracts.csv are ≤highestPartnerId - All
ouId/activityIdvalues in ropa CSVs are within their respectivehighest*bounds - All
activityIdsreferenced in contracts.csv exist in at least one ropa CSV
Import Process
JSON import
1. Validate envelope against Zod schema (exportVersion, required fields)
2. If org already exists: delete templates → ROPAs → org
3. Create ROPA documents — one per locale → collect { locale → ObjectId } map
4. Create Organization — links ropas[] via the locale map
5. Create Templates — each gets a new _id linked to the new org _id
6. Patch org.templates[] with the new templateId references
CSV ZIP import
1. Parse organization.csv → orgData
2. Parse locales.csv → locales[]
3. Parse partners.csv → partners[]
4. Parse contracts.csv → contracts[]
5. Parse ropa-{locale}.csv for each locale → ropas[]
6. Parse templates.json if present → templates[]
7. Reconstruct JSON envelope and delegate to importOrganization()
Step 7 reuses the identical JSON import pipeline, so validation, delete-before-recreate, and creation order are the same for both formats.
If any step fails, already-created documents are not automatically rolled back. To recover from a partial import, delete the partially-created organization and retry.
Migrating Between Environments
# Export from staging (JSON)
curl -H "x-admin-secret: $STAGING_SECRET" \
"https://staging.example.com/api/admin/org/export?shortName=acme" \
-o acme-export.json
# Import into production
curl -X POST \
-H "x-admin-secret: $PROD_SECRET" \
-H "Content-Type: application/json" \
-d @acme-export.json \
"https://prod.example.com/api/admin/org/import"
Source Files
| File | Purpose |
|---|---|
src/services/exportService.js | exportOrganization (JSON) and exportOrganizationAsCsvZip (CSV ZIP) |
src/services/importService.js | importOrganization (JSON) and importOrganizationFromCsvZip (CSV ZIP) |
src/services/csvService.js | CSV serializers/deserializers for all entity types |
src/lib/utils/fileChecks.js | isValidJson, isValidZip, zipCheck |
src/app/api/admin/org/export/route.js | Export route handler (both formats) |
src/app/api/admin/org/import/route.js | Import route handler (both formats) |