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:
- store the provider config in your own backend as an encrypted integration record
- decrypt that config in the browser through the repository layer
- pass the decrypted config into an external datasource client
- authenticate against the provider API
- 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.tssrc/lib/providers/linear.tssrc/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.