Schema Manifest¶
The adapter consumes a versioned JSON manifest.
That manifest is generated by e2ee-client-backend and describes:
- auth route paths and session settings
- configured entities and REST route bases
- field mappings between entity and remote records
- encryption metadata per field
- expected database tables
- optional realtime configuration
Version 4 is the current manifest contract.
If you need a file-by-file view of what gets generated, who consumes it, and what is safe to edit manually, see File Lifecycle.
Client Schema Export¶
The adapter can also export a separate generated schema file for client-side consumption, but it does not read that generated file back at runtime.
export-expected-schema writes a JSON representation of the combined DB schema
config plus encrypted schema overlay. It is not SQL DDL and it is not a
migration file.
The DB schema config is the structural source of truth for export generation. The encrypted schema config is a second, user-authored file that adds decrypted encrypted-field structure and API naming overrides. Auth is still implicit and fixed by the adapter.
If you also pass --typescript-out, the adapter writes a generated TypeScript
companion module alongside the JSON export. For now, TypeScript is the only
supported language target.
Example export workflow:
e2ee-backend-adapter-cli export-expected-schema \
--db-schema-config ./e2ee-backend.db-schema.json \
--encrypted-schema-config ./e2ee-backend.encrypted-schema.json \
--api graphql \
--out ./generated/expected-schema.json \
--typescript-out ./generated/e2ee-client-bindings.ts
DB Schema Config File¶
The DB schema config file is a generated JSON file consumed during schema export. The runtime server does not read it when serving requests, and database diffing still works from the manifest/runtime contract.
Use it to carry the database-derived structural model: entity names, table metadata, inferred non-encrypted field schemas, and encrypted field placeholders.
The common case is an encrypted field like config that is stored in the
database as ciphertext and nonce columns, but should appear in generated client
types as a structured object such as PlanderaConfig or DashboardConfig.
The top-level shape is:
{
"name": "my-backend",
"entities": [
{
"name": "integration",
"tableName": "integrations",
"idPath": "id",
"database": {
"primaryKey": "id",
"columns": [
{ "columnName": "id", "nullable": false, "sqlType": "UUID" },
{ "columnName": "config_ciphertext", "nullable": true, "sqlType": "BYTEA" },
{ "columnName": "config_nonce", "nullable": true, "sqlType": "BYTEA" }
]
},
"fields": [
{
"encrypted": true,
"entityPath": "config",
"entitySchema": {
"nullable": true,
"schema": { "type": "unknown" }
},
"remotePath": "configEnvelope"
},
{
"encrypted": false,
"entityPath": "displayName",
"entitySchema": { "schema": { "type": "string" } }
}
]
}
]
}
Encrypted Schema Config File¶
The encrypted schema config is the user-authored overlay. It defines the rich logical structure of encrypted fields and any API naming overrides that the DB cannot infer.
{
"entityApiOverrides": [
{
"tableName": "integrations",
"graphql": {
"createMutation": "createIntegrationRecord",
"deleteMutation": "deleteIntegration",
"getByIdQuery": "integrationRecord",
"listQuery": "integrationRecords",
"updateMutation": "updateIntegrationRecord"
}
}
],
"encryptedFields": [
{
"tableName": "integrations",
"entityPath": "config",
"entitySchema": { "ref": "PlanderaConfig", "nullable": true }
}
],
"types": {
"PlanderaConfig": {
"schema": {
"type": "object",
"additionalProperties": false,
"properties": {
"apiUrl": { "schema": { "type": "string" } },
"authHash": {
"nullable": true,
"schema": { "type": "string" }
},
"username": { "schema": { "type": "string" } }
}
}
}
}
}
Top-Level Keys¶
name: backend/schema name used in generated outputs, in the DB schema fileentities: exported entities and their DB mapping, in the DB schema filecustomOperations: optional non-entity REST/GraphQL operations, in the encrypted schema fileentityApiOverrides: optional per-entity GraphQL or REST naming overrides, in the encrypted schema fileencryptedFields: encrypted field logical type overrides, in the encrypted schema filetypes: named reusable schema nodes that other entries can reference with{"ref": "TypeName"}
Entity Field Entries¶
Each encrypted field entry supports:
entityName: optional exported entity name matchertableName: optional table-name matcherentityPath: logical client-side field path, such asconfigentitySchema: schema node for the decrypted entity-side valueremotePath: optional remote/API field name override, such asconfigEnveloperemoteSchema: optional remote-side schema override when the API shape differsstrategyId: optional encryption strategy override
The encrypted schema config can also include optional per-entity graphql or
rest override blocks when the backend API names do not follow adapter
conventions.
Custom Operations¶
Use customOperations when you need generated frontend helpers for backend
endpoints that are not CRUD operations on an exported entity.
Each custom operation entry supports:
name: logical operation name used in generated TypeScript aliases and helper mapsrequestSchema: optional schema node describing the input payloadresponseSchema: optional schema node describing the returned payloadrest: optional REST metadata overridesgraphql: optional GraphQL metadata overrides
REST defaults to POST /operations/<kebab-case-name>.
GraphQL defaults to:
fieldName: camel-cased operation nameoperationType:mutationinputTypeName:<PascalCaseName>Input!whenrequestSchemais present
If your GraphQL response shape cannot be converted into a selection set from the
schema node alone, provide graphql.selectionSet explicitly in the encrypted
schema config.
These entries are exported into expected-schema.json and the generated
TypeScript bindings, but you still register the actual backend handlers in your
host application.
Scaffolding From The Database¶
You can scaffold the DB schema config from Postgres metadata:
e2ee-backend-adapter-cli generate-db-schema-config \
--database-url postgres://postgres:postgres@localhost:5432/app \
--name my-backend \
--out ./e2ee-backend.db-schema.json
This scaffolds tables, columns, primary keys, basic scalar types, and encrypted
*_ciphertext/*_nonce pairs into the DB schema file. It does not replace
manual modeling of the final decrypted object structure in the encrypted schema
file.
Schema Nodes¶
Each schema node can either inline a schema:
or reference a named type:
Nodes support optional nullable and optional flags on top of the base
schema.
Supported schema descriptors are:
stringnumberwith optionalinteger: truebooleanliteralenumobjectrecordarrayuniondiscriminatedUnionunknown
Example: Reusable Encrypted Object Type¶
{
"types": {
"PlanderaConfig": {
"schema": {
"type": "object",
"additionalProperties": false,
"properties": {
"apiUrl": { "schema": { "type": "string" } },
"authHash": {
"nullable": true,
"schema": { "type": "string" }
},
"encryptionKey": {
"nullable": true,
"schema": { "type": "string" }
},
"providerSecret": {
"nullable": true,
"schema": { "type": "string" }
},
"username": { "schema": { "type": "string" } }
}
}
}
},
"encryptedFields": [
{
"entityName": "integration",
"entityPath": "config",
"entitySchema": { "ref": "PlanderaConfig" }
}
]
}
This causes the generated TypeScript bindings to emit config as a structured
object type instead of the fallback Record<string, unknown> | null.
Example: Discriminated Dashboard Config¶
{
"types": {
"DashboardTile": {
"schema": {
"type": "discriminatedUnion",
"discriminator": "type",
"options": [
{
"schema": {
"type": "object",
"additionalProperties": false,
"properties": {
"type": {
"schema": { "type": "literal", "value": "markdown" }
},
"title": { "schema": { "type": "string" } }
}
}
}
]
}
},
"DashboardConfig": {
"schema": {
"type": "object",
"additionalProperties": false,
"properties": {
"version": {
"schema": { "type": "literal", "value": 1 }
},
"tiles": {
"schema": {
"type": "array",
"items": { "ref": "DashboardTile" }
}
}
}
}
}
},
"encryptedFields": [
{
"tableName": "dashboards",
"entityPath": "config",
"entitySchema": { "ref": "DashboardConfig" }
}
]
}
This is the pattern to use when your encrypted config contains nested arrays, discriminated unions, or other richer client-side structure that the database cannot express directly.
The exported shape is:
{
"expectedSchema": {
"api": {
"rest": {
"baseUrl": "/api",
"defaultHeaders": {
"accept": "application/json"
}
},
"type": "rest"
},
"authTables": ["users", "sessions"],
"entities": [
{
"api": {
"rest": {
"allowCreate": true,
"allowDelete": true,
"allowGetById": true,
"allowList": true,
"allowUpdate": true,
"basePath": "/entities/note"
},
"type": "rest"
},
"fields": [
{
"encrypted": true,
"entityPath": "content",
"entityType": "string",
"nullable": false,
"optional": false,
"remotePath": "ciphertext",
"remoteType": "string",
"strategyId": "aes-256-gcm"
},
{
"encrypted": false,
"entityPath": "id",
"entityType": "string",
"nullable": false,
"optional": false,
"remotePath": "id",
"remoteType": "string"
}
],
"idPath": "id",
"name": "note",
"primaryKey": "id",
"tableName": "notes"
}
],
"entityTables": [
{
"columns": [
{
"columnName": "ciphertext",
"nullable": false,
"sqlType": "TEXT"
},
{
"columnName": "id",
"nullable": false,
"sqlType": "TEXT"
}
],
"primaryKey": "id",
"tableName": "notes"
}
]
}
}
This describes:
- the API family this generated schema targets
- the default REST base URL or GraphQL endpoint path and headers exported for generated clients
- auth-related tables the adapter expects to exist
- entity tables the adapter expects to exist
- explicit database columns and SQL types for each expected entity table
- the expected primary key field for each entity table
- per-entity default REST route metadata or GraphQL operation names derived from the adapter config
- per-entity field metadata including logical field names, remote field names, data types, nullability, optionality, and whether a field is e2ee-encrypted
- optional richer field schema metadata such as
entitySchemaandremoteSchemawhen a schema config file was provided during export
For GraphQL exports, the api and per-entity api blocks switch to GraphQL metadata instead:
{
"expectedSchema": {
"api": {
"graphql": {
"defaultHeaders": {
"accept": "application/json"
},
"endpointPath": "/graphql"
},
"type": "graphql"
},
"entities": [
{
"api": {
"graphql": {
"allowCreate": true,
"allowDelete": true,
"allowGetById": true,
"allowList": true,
"allowUpdate": true,
"createMutation": "createNote",
"deleteMutation": "deleteNote",
"getByIdQuery": "note",
"listQuery": "notes",
"updateMutation": "updateNote"
},
"type": "graphql"
}
}
]
}
}
The generated TypeScript module exports:
SessionUser<EntityName>Entity,<EntityName>RemoteRecord, and<EntityName>Idtype aliases<OperationName>Requestand<OperationName>Responsetype aliases for custom operationscreateRestTransport(...)andcreateGraphqlTransport(...)createRestAuthConfig(...)andcreateGraphqlAuthConfig(...)createEntitySchemas(...)createRestModels(...)andcreateGraphqlModels(...)createRestCustomOperations(...)andcreateGraphqlCustomOperations(...)createRestCrudAdapters(...)andcreateGraphqlCrudAdapters(...)
That lets a client app import typed auth and model helpers directly instead of
rewriting SessionUser, entity types, route or operation wiring, or default
transport configuration by hand. createRestModels(...) and
createGraphqlModels(...) wire the generated model map automatically so apps
can keep using createE2eeBackend(...) with the generated schema bindings.