Skip to content

E2eeBackend

What It Is

E2eeBackend is the highest-level package API.

Use it when you do not want to manually wire together:

  • password-auth flows
  • browser persistence for managed secrets
  • a contextResolver that injects the active encryption key
  • model registration
  • client lookup and lazy service creation

It sits above defineEntityModel(...) and createEntityClient(...) and gives you one long-lived stateful object for the frontend.

What It Manages

An E2eeBackend instance can manage these concerns together:

  • password-based login and registration through built-in auth configuration
  • storage of the managed password and derived encryption key
  • automatic contextResolver.resolve(...) key injection for encrypted fields
  • registration of model-backed clients
  • registration of arbitrary lazily created services

That makes it a good fit for browser applications that want one E2EE entrypoint rather than several smaller primitives.

Storage Strategy

The browser persistence strategy is configurable through the E2eeBackendStorageStrategy enum.

Available options:

  • E2eeBackendStorageStrategy.LocalStorage
  • E2eeBackendStorageStrategy.SessionStorage
  • E2eeBackendStorageStrategy.Memory
  • a custom store implementing load() and save()

The package default is local storage.

Minimal Examples

Each example below is self-contained and can be copied independently.

Minimal GraphQL Example

import {
  E2eeEncryptionStrategy,
  E2eeBackendStorageStrategy,
  type EncryptedFieldValue,
  GraphqlCrudAdapter,
  createAes256GcmStrategy,
  createE2eeBackend,
  createGraphqlPasswordAuthConfig,
  createGraphqlTransport,
  createStrategyRegistry,
  defineClientModel,
  defineEntityModel,
  field,
} from "e2ee-client-backend";

type SessionUser = {
  email: string;
  id: string;
};

type NoteRemoteRecord = {
  content: EncryptedFieldValue;
  id: string;
  title: string;
};

const KDF_SALT = `
  query KdfSalt($email: String!) {
    kdfSalt(email: $email)
  }
`;

const LOGIN = `
  mutation Login($email: String!, $authKeyMaterialHex: String!) {
    login(email: $email, authKeyMaterialHex: $authKeyMaterialHex) {
      ok
      message
      user {
        id
        email
      }
    }
  }
`;

const LOGOUT = `
  mutation Logout {
    logout
  }
`;

const REFRESH = `
  mutation RefreshSession {
    refreshSession {
      ok
      message
      user {
        id
        email
      }
    }
  }
`;

const REGISTER_BEGIN = `
  mutation RegisterBegin($email: String!) {
    registerBegin(email: $email) {
      kdfSaltBase64
    }
  }
`;

const REGISTER_COMPLETE = `
  mutation RegisterComplete($email: String!, $authKeyMaterialHex: String!) {
    registerComplete(email: $email, authKeyMaterialHex: $authKeyMaterialHex) {
      ok
      message
      user {
        id
        email
      }
    }
  }
`;

const noteModel = defineEntityModel({
  cacheCollection: "notes",
  fields: {
    id: field.string(),
    title: field.string(),
    content: field.string().encrypted(),
  },
  idField: "id",
  name: "note",
});

const graphqlTransport = createGraphqlTransport(async ({ document, kind, variables }) => {
  const response = await fetch("/graphql", {
    body: JSON.stringify({ query: String(document), variables }),
    headers: { "content-type": "application/json" },
    method: "POST",
  });
  const payload = await response.json() as {
    data?: unknown;
    errors?: Array<{ message: string }>;
  };

  if (payload.errors?.length) {
    throw new Error(payload.errors[0].message);
  }

  return payload.data;
});

const auth = createGraphqlPasswordAuthConfig<SessionUser>({
  documents: {
    getKdfSalt: KDF_SALT,
    login: LOGIN,
    logout: LOGOUT,
    refresh: REFRESH,
    registerBegin: REGISTER_BEGIN,
    registerComplete: REGISTER_COMPLETE,
  },
  transport: graphqlTransport,
});

const graphqlAdapter = new GraphqlCrudAdapter<NoteRemoteRecord, string>(
  graphqlTransport,
  {
    create: {
      buildVariables: (input) => ({ input }),
      document: `
        mutation CreateNote($input: NoteInput!) {
          createNote(input: $input) {
            id
            title
            content
          }
        }
      `,
      select: (result) => (result as { createNote: NoteRemoteRecord }).createNote,
    },
    delete: {
      buildVariables: (id) => ({ id }),
      document: `
        mutation DeleteNote($id: ID!) {
          deleteNote(id: $id)
        }
      `,
    },
    getById: {
      buildVariables: (id) => ({ id }),
      document: `
        query Note($id: ID!) {
          note(id: $id) {
            id
            title
            content
          }
        }
      `,
      select: (result) => (result as { note: NoteRemoteRecord | null }).note,
    },
    list: {
      document: `
        query Notes {
          notes {
            id
            title
            content
          }
        }
      `,
      select: (result) => (result as { notes: NoteRemoteRecord[] }).notes,
    },
    update: {
      buildVariables: (id, input) => ({ id, input }),
      document: `
        mutation UpdateNote($id: ID!, $input: NoteInput!) {
          updateNote(id: $id, input: $input) {
            id
            title
            content
          }
        }
      `,
      select: (result) => (result as { updateNote: NoteRemoteRecord }).updateNote,
    },
  },
);

const graphqlBackend = createE2eeBackend({
  auth,
  defaultStrategyId: E2eeEncryptionStrategy.Aes256Gcm,
  models: {
    notes: defineClientModel({
      adapter: graphqlAdapter,
      schema: noteModel,
    }),
  },
  storage: E2eeBackendStorageStrategy.LocalStorage,
  storageKey: "my-app.e2ee.v1",
  strategies: createStrategyRegistry(createAes256GcmStrategy()),
});

await graphqlBackend.loginWithPassword("ops@example.com", "top-secret-password");

const graphqlNotes = graphqlBackend.getClient("notes");

await graphqlNotes.create({
  content: "Encrypted text",
  id: crypto.randomUUID(),
  title: "First note",
});

Minimal GraphQL Example With Apollo Client

import {
  E2eeEncryptionStrategy,
  E2eeBackendStorageStrategy,
  type EncryptedFieldValue,
  GraphqlCrudAdapter,
  createAes256GcmStrategy,
  createE2eeBackend,
  createGraphqlPasswordAuthConfig,
  createGraphqlTransport,
  createStrategyRegistry,
  defineClientModel,
  defineEntityModel,
  field,
} from "e2ee-client-backend";

import {
  gql,
  type ApolloClient,
  type NormalizedCacheObject,
} from "@apollo/client";

type SessionUser = {
  email: string;
  id: string;
};

type NoteRemoteRecord = {
  content: EncryptedFieldValue;
  id: string;
  title: string;
};

const noteModel = defineEntityModel({
  cacheCollection: "notes",
  fields: {
    id: field.string(),
    title: field.string(),
    content: field.string().encrypted(),
  },
  idField: "id",
  name: "note",
});

const GET_KDF_SALT = gql`
  query KdfSalt($email: String!) {
    kdfSalt(email: $email)
  }
`;

const LOGIN = gql`
  mutation Login($email: String!, $authKeyMaterialHex: String!) {
    login(email: $email, authKeyMaterialHex: $authKeyMaterialHex) {
      ok
      message
      user {
        id
        email
      }
    }
  }
`;

const LOGOUT = gql`
  mutation Logout {
    logout
  }
`;

const REFRESH = gql`
  mutation RefreshSession {
    refreshSession {
      ok
      message
      user {
        id
        email
      }
    }
  }
`;

const REGISTER_BEGIN = gql`
  mutation RegisterBegin($email: String!) {
    registerBegin(email: $email) {
      kdfSaltBase64
    }
  }
`;

const REGISTER_COMPLETE = gql`
  mutation RegisterComplete($email: String!, $authKeyMaterialHex: String!) {
    registerComplete(email: $email, authKeyMaterialHex: $authKeyMaterialHex) {
      ok
      message
      user {
        id
        email
      }
    }
  }
`;

const CREATE_NOTE = gql`
  mutation CreateNote($input: NoteInput!) {
    createNote(input: $input) {
      id
      title
      content
    }
  }
`;

const DELETE_NOTE = gql`
  mutation DeleteNote($id: ID!) {
    deleteNote(id: $id)
  }
`;

const GET_NOTE = gql`
  query Note($id: ID!) {
    note(id: $id) {
      id
      title
      content
    }
  }
`;

const LIST_NOTES = gql`
  query Notes {
    notes {
      id
      title
      content
    }
  }
`;

const UPDATE_NOTE = gql`
  mutation UpdateNote($id: ID!, $input: NoteInput!) {
    updateNote(id: $id, input: $input) {
      id
      title
      content
    }
  }
`;

function createApolloGraphqlTransport(
  client: ApolloClient<NormalizedCacheObject>,
) {
  return createGraphqlTransport(async ({ document, kind, variables }) => {
    if (kind === "mutation") {
      const { data } = await client.mutate({
        mutation: document,
        variables,
      });
      return data;
    }

    const { data } = await client.query({
      fetchPolicy: "network-only",
      query: document,
      variables,
    });
    return data;
  });
}

declare const apolloClient: ApolloClient<NormalizedCacheObject>;

const auth = createGraphqlPasswordAuthConfig<SessionUser>({
  documents: {
    getKdfSalt: GET_KDF_SALT,
    login: LOGIN,
    logout: LOGOUT,
    refresh: REFRESH,
    registerBegin: REGISTER_BEGIN,
    registerComplete: REGISTER_COMPLETE,
  },
  transport: createApolloGraphqlTransport(apolloClient),
});

const apolloAdapter = new GraphqlCrudAdapter<NoteRemoteRecord, string>(
  createApolloGraphqlTransport(apolloClient),
  {
    create: {
      buildVariables: (input) => ({ input }),
      document: CREATE_NOTE,
      select: (result) => (result as { createNote: NoteRemoteRecord }).createNote,
    },
    delete: {
      buildVariables: (id) => ({ id }),
      document: DELETE_NOTE,
    },
    getById: {
      buildVariables: (id) => ({ id }),
      document: GET_NOTE,
      select: (result) => (result as { note: NoteRemoteRecord | null }).note,
    },
    list: {
      document: LIST_NOTES,
      select: (result) => (result as { notes: NoteRemoteRecord[] }).notes,
    },
    update: {
      buildVariables: (id, input) => ({ id, input }),
      document: UPDATE_NOTE,
      select: (result) => (result as { updateNote: NoteRemoteRecord }).updateNote,
    },
  },
);

const apolloBackend = createE2eeBackend({
  auth,
  defaultStrategyId: E2eeEncryptionStrategy.Aes256Gcm,
  models: {
    notes: defineClientModel({
      adapter: apolloAdapter,
      schema: noteModel,
    }),
  },
  storage: E2eeBackendStorageStrategy.LocalStorage,
  storageKey: "my-app.e2ee.v1",
  strategies: createStrategyRegistry(createAes256GcmStrategy()),
});

await apolloBackend.loginWithPassword("ops@example.com", "top-secret-password");

const apolloNotes = apolloBackend.getClient("notes");

await apolloNotes.create({
  content: "Encrypted text",
  id: crypto.randomUUID(),
  title: "First note",
});

Minimal REST Example

import {
  E2eeEncryptionStrategy,
  E2eeBackendStorageStrategy,
  type EncryptedFieldValue,
  RestCrudAdapter,
  createAes256GcmStrategy,
  createE2eeBackend,
  createFetchRestTransport,
  createRestPasswordAuthConfig,
  createStrategyRegistry,
  defineClientModel,
  defineEntityModel,
  field,
} from "e2ee-client-backend";

type SessionUser = {
  email: string;
  id: string;
};

type NoteRemoteRecord = {
  content: EncryptedFieldValue;
  id: string;
  title: string;
};

const noteModel = defineEntityModel({
  cacheCollection: "notes",
  fields: {
    id: field.string(),
    title: field.string(),
    content: field.string().encrypted(),
  },
  idField: "id",
  name: "note",
});

const restTransport = createFetchRestTransport({
  baseUrl: "/api",
  defaultHeaders: {
    accept: "application/json",
  },
});

const auth = createRestPasswordAuthConfig<SessionUser>({
  transport: restTransport,
});

const restAdapter = new RestCrudAdapter<NoteRemoteRecord, string>(restTransport, {
  create: { path: "/notes" },
  delete: { path: (id) => `/notes/${id}` },
  getById: { path: (id) => `/notes/${id}` },
  list: { path: "/notes" },
  update: { path: (id) => `/notes/${id}` },
});

const restBackend = createE2eeBackend({
  auth,
  defaultStrategyId: E2eeEncryptionStrategy.Aes256Gcm,
  models: {
    notes: defineClientModel({
      adapter: restAdapter,
      schema: noteModel,
    }),
  },
  storage: E2eeBackendStorageStrategy.LocalStorage,
  storageKey: "my-app.e2ee.v1",
  strategies: createStrategyRegistry(createAes256GcmStrategy()),
});

await restBackend.loginWithPassword("ops@example.com", "top-secret-password");

const restNotes = restBackend.getClient("notes");

await restNotes.create({
  content: "Encrypted text",
  id: crypto.randomUUID(),
  title: "First note",
});

The important point is that you never provide a manual contextResolver. The backend stores the managed encryption key and injects it automatically when repository operations need to encrypt or decrypt fields.

The built-in auth configuration creates the password auth adapter internally. You only provide transport-level auth configuration plus the CRUD adapter for each model.

defaultStrategyId applies to every registered model in that backend unless a field or lower-level schema explicitly overrides the strategy.

Auth Flow

If you provide auth configuration, E2eeBackend exposes the same high-level auth operations directly:

  • beginRegistration(email)
  • completeRegistrationWithPassword(email, password, kdfSaltBase64)
  • registerWithPassword(email, password)
  • loginWithPassword(email, password)
  • refreshSession()
  • logout()

Successful password-based auth stores the managed password and derived encryption key according to the configured storage strategy.

Model Registration

You can register models either during creation or later.

Creation-time registration is best when you want strong inference from the initial object literal.

const backend = createE2eeBackend({
  models: {
    notes: defineClientModel({ adapter, schema: noteModel }),
  },
});

Runtime registration is useful when models are assembled in steps.

const backend = createE2eeBackend();

backend.registerModel(
  "notes",
  defineClientModel({ adapter, schema: noteModel }),
);

Use getClient("notes") to retrieve the lazily created model client.

Service Registration

You can also register non-repository services on the same object.

backend.registerService("externalApis", () => ({
  plandera: createPlanderaExternalApiClient(),
}));

const externalApis = backend.getService("externalApis");

This is useful for app-level provider registries, external datasource clients, or other services that should be created from the same root backend object.

When To Use Lower-Level APIs

Use E2eeBackend when you want one object to own the application E2EE workflow.

Drop to lower-level APIs when:

  • you want repository construction without managed auth or browser storage
  • you want explicit control over contextResolver
  • you are building a library and do not want a stateful browser-oriented abstraction

In those cases, use createEntityClient(...) or createEntityRepository(...) directly.