Skip to content

Provider model

A Notewright instance composes exactly one notes source and one follow-up sink. Both are providers: small packages that implement a plain TypeScript contract and declare the configuration they read.

A notes source implements NotesSourceProvider:

interface NotesSourceProvider {
readonly id: string;
readonly configSchema: ConfigSchema;
listUpdatedNotes(since: Date, ctx: ProviderCtx): Promise<NoteSummary[]>;
getNoteWithTranscript(id: string, ctx: ProviderCtx): Promise<NoteDetail>;
}

A follow-up sink implements FollowUpProvider:

interface FollowUpProvider {
readonly id: string;
readonly configSchema: ConfigSchema;
createFollowUp(item: FollowUpItem, ctx: ProviderCtx): Promise<CreateFollowUpResult>;
}

Both are plain and Promise-shaped. No Effect types ever appear in a provider contract.

Each provider declares the env keys it needs, split by sensitivity and whether they are required:

type ConfigSchema = {
readonly secrets: ReadonlyArray<string>; // sensitive, required (API keys)
readonly vars: ReadonlyArray<string>; // non-sensitive, required
readonly optionalVars?: ReadonlyArray<string>; // non-sensitive, optional (have defaults)
};

At runtime the Worker collects exactly those keys (across all three lists) from env and hands them to the provider as ctx.config (a Record<string, string>). A provider only ever sees the configuration it declared.

doctor requires every secrets and vars key to be set before deploy, but never requires optionalVars (the provider falls back to a code default when one is unset). Put a key in optionalVars only when omitting it is safe.

Every provider method receives a context:

type ProviderCtx = {
readonly config: Record<string, string>; // only the declared keys
readonly fetch: typeof fetch; // use this for outbound HTTP
readonly log: (fields: Record<string, unknown>) => void;
};

A provider method signals failure by throwing a ProviderError with a kind:

throw new ProviderError({ message: "Upstream returned 503", kind: "transient", status: 503 });
  • kind: "transient" is retried by the pipeline (rate limits, 5xx).
  • kind: "permanent" is not retried (bad input, auth failures).

Providers never return or throw Effect errors; the core normalizes thrown ProviderErrors internally.

ProviderKindPackage
GranolaNotes source@notewright/granola-notes-provider
LinearFollow-up sink@notewright/linear-follow-up-provider
NoopFollow-up sink@notewright/noop-follow-up-provider

To build your own, see Writing a provider.