Skip to content

External Datasources

What This Is For

Use the external datasource API when your application stores encrypted integration config locally, but then needs to call a live third-party API with the decrypted config.

Typical examples:

  • a task system such as Plandera
  • a project tracker
  • a note or ticketing API

This is a different problem from storing your own encrypted application records.

  • Use createEntityClient(...) for your own persisted encrypted entities.
  • Use createExternalE2eeApiClient(...) for live third-party API access.

Mental Model

The usual flow looks like this:

  1. store the provider config in your own backend as an encrypted integration record
  2. decrypt that config in the browser through the repository layer
  3. pass the decrypted config into an external datasource client
  4. authenticate against the provider API
  5. fetch and map projects, tasks, or other provider records into app-facing types

The shared package stays generic. Provider-specific endpoint paths, auth semantics, and response mapping belong in your application code.

Minimal Example

This example shows the smallest useful provider built on top of the package exports.

import {
  createExternalE2eeApiClient,
  createFetchRestTransport,
  type ExternalE2eeApiProvider,
} from "e2ee-client-backend";

type ExampleConfig = {
  apiUrl: string;
  apiToken: string;
};

type ExampleProject = {
  id: string;
  name: string;
};

type ExampleTask = {
  id: string;
  projectId?: string;
  title: string;
  done: boolean;
};

function createExampleTransport(config: ExampleConfig) {
  return createFetchRestTransport({
    baseUrl: config.apiUrl,
    defaultHeaders: {
      Authorization: `Bearer ${config.apiToken}`,
      "Content-Type": "application/json",
    },
  });
}

function createExampleProvider(): ExternalE2eeApiProvider<
  ExampleConfig,
  string,
  ExampleProject,
  ExampleTask,
  undefined,
  string | undefined
> {
  return {
    async authenticate(config) {
      return config.apiToken;
    },
    async listProjects(config) {
      const transport = createExampleTransport(config);
      const response = await transport.request<{ items: Array<{ id: string; name: string }> }>({
        method: "GET",
        path: "/projects",
      });

      return response.items.map((project) => ({
        id: project.id,
        name: project.name,
      }));
    },
    async listTasks(config, _session, projectId) {
      const transport = createExampleTransport(config);
      const query = projectId ? `?projectId=${encodeURIComponent(projectId)}` : "";
      const response = await transport.request<{
        items: Array<{ id: string; projectId?: string; title: string; completed: boolean }>;
      }>({
        method: "GET",
        path: `/tasks${query}`,
      });

      return response.items.map((task) => ({
        done: task.completed,
        id: task.id,
        projectId: task.projectId,
        title: task.title,
      }));
    },
    async validateAccess(config, _session) {
      const transport = createExampleTransport(config);
      await transport.request({
        method: "GET",
        path: "/projects",
      });
    },
  };
}

const exampleApi = createExternalE2eeApiClient(createExampleProvider());

const projects = await exampleApi.listProjects({
  apiToken: "token-from-decrypted-config",
  apiUrl: "https://api.example.com",
});

What The Provider Methods Mean

  • authenticate(config) returns session material for the provider, such as a bearer token.
  • listProjects(config, session, query) loads and maps project-like records.
  • listTasks(config, session, query) loads and maps task-like records.
  • validateAccess(config, session) is optional and is useful for connection tests when a user saves an integration.

The generic client will authenticate first and then pass the resulting session into later calls.

Where Provider Logic Should Live

Keep provider-specific logic in your application, not in the shared package.

That means this code should usually live in an app module such as:

  • src/lib/providers/plandera.ts
  • src/lib/providers/linear.ts
  • src/lib/providers/jira.ts

The shared package should only provide the generic contracts and utilities.

Relationship To Encrypted Integrations

In a real application, the config object passed into the external datasource client is usually the decrypted payload from an encrypted integration record.

That gives you a clean split:

  • repository layer manages encrypted persistence of provider config
  • external datasource layer manages live API access using that config

This separation is the reason the package exposes both client families.