Skip to content

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 file
  • entities: exported entities and their DB mapping, in the DB schema file
  • customOperations: optional non-entity REST/GraphQL operations, in the encrypted schema file
  • entityApiOverrides: optional per-entity GraphQL or REST naming overrides, in the encrypted schema file
  • encryptedFields: encrypted field logical type overrides, in the encrypted schema file
  • types: 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 matcher
  • tableName: optional table-name matcher
  • entityPath: logical client-side field path, such as config
  • entitySchema: schema node for the decrypted entity-side value
  • remotePath: optional remote/API field name override, such as configEnvelope
  • remoteSchema: optional remote-side schema override when the API shape differs
  • strategyId: 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 maps
  • requestSchema: optional schema node describing the input payload
  • responseSchema: optional schema node describing the returned payload
  • rest: optional REST metadata overrides
  • graphql: optional GraphQL metadata overrides

REST defaults to POST /operations/<kebab-case-name>.

GraphQL defaults to:

  • fieldName: camel-cased operation name
  • operationType: mutation
  • inputTypeName: <PascalCaseName>Input! when requestSchema is 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:

{
    "nullable": true,
    "schema": { "type": "string" }
}

or reference a named type:

{
    "ref": "PlanderaConfig"
}

Nodes support optional nullable and optional flags on top of the base schema.

Supported schema descriptors are:

  • string
  • number with optional integer: true
  • boolean
  • literal
  • enum
  • object
  • record
  • array
  • union
  • discriminatedUnion
  • unknown

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 entitySchema and remoteSchema when 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>Id type aliases
  • <OperationName>Request and <OperationName>Response type aliases for custom operations
  • createRestTransport(...) and createGraphqlTransport(...)
  • createRestAuthConfig(...) and createGraphqlAuthConfig(...)
  • createEntitySchemas(...)
  • createRestModels(...) and createGraphqlModels(...)
  • createRestCustomOperations(...) and createGraphqlCustomOperations(...)
  • createRestCrudAdapters(...) and createGraphqlCrudAdapters(...)

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.