# KIM through the service's own client module

The repository's architectural decision, rationale and consequences.

# KIM through the service's own client module

**Status:** accepted · **Date:** 2026-11-01

**Enforcement:** `packages/api/src/platform/kim/` ships the `KimTransport` interface and both implementations; `createKimTransport()` picks `fake` or `smtp` by `env.KIM_TRANSPORT`; `packages/api/src/features/reports/reports.jobs.ts` calls the transport only through this interface; `reports.deliver.test.ts` asserts the fake delivery path, the lease retry on failure, the KEK-wrapped credentials and the print-in-memory invariant.

## Context [#context]

Ticket 56 ships the one send path a nurse touches in v1 (US-31, F4, C5): a report leaves the device over KIM, or as a share link, or straight to the printer. KIM is the Telematikinfrastruktur's secure mail: delivery over a service-run SMTP bridge with STARTTLS 587 to a specialist operator, authenticated with the service's SMC-B certificate. There are two things we cannot do from the Cloudflare Workers / Expo runtime:

- Open a raw SMTP connection from the device or a Worker — neither environment exposes raw TCP + STARTTLS, and we never want the device holding KIM credentials.
- Call a vendor-hosted REST API from either side — none of the KIM bridges we evaluated expose one as of this writing.

A third option is a service-run KIM bridge (self-hosted container, Cloudflare Tunnel to the bridge, or a managed KIM-SMTP relay) addressed by a per-organization address pair + credentials. The bridge speaks SMTP + STARTTLS + S/MIME, which Workers can reach over `fetch`-less transports is still [TBD], but a `worker-mailer` SMTP client over an outbound TCP connection is the current working plan (plan §1.8). The bridge is not provisioned in v1; the API ships the interface so the owning team can plug the bridge in without touching the feature code.

Dev and tests must still exercise the whole flow without hitting a bridge.

## Decision [#decision]

Reports feature talks to KIM through a tiny internal `KimTransport` interface implemented in two places:

- `kim.fake.ts` — selected only by `KIM_TRANSPORT=fake` with `WUNDKLAR_DEV_KIM_INBOX=1`, a local `BETTER_AUTH_URL`, and a non-production runtime. It reports `delivered` with the supplied Message-ID and keeps the decrypted PDF in memory. `/dev/kim-inbox` additionally requires `VITE_WUNDKLAR_DEV_KIM_INBOX=1` in a development build.
- `kim.smtp.ts` — the default transport (`KIM_TRANSPORT=smtp`): a placeholder that reports a retryable failure code until the service-run bridge is wired in. The leased `reportsKim` job retries with backoff — nothing is lost, the operator sees the failure in `kim_outbox.last_error`.

Per-organization credentials (SMTP host, port, user, password) are KEK-wrapped under the organisation's ODK with purpose `kim` and stored in `care_service.kim_config_ct`. `reports.kimConfig` returns only the public KIM address + a `hasCredentials` boolean; the ciphertext never leaves the server. `reports.kimTest` runs a single `KimTransport.send` and reports a status code only.

### Uncertain delivery and retries [#uncertain-delivery-and-retries]

Before invoking the transport, the job records an `attempt` delivery event under its fenced lease and supplies a stable Message-ID derived from the delivery ID. The bridge does not yet promise Message-ID deduplication. If a send may have been accepted but its outcome is lost, or a lease is reclaimed with an unfinished attempt, the job records `unknown` and stops. The nurse must check the recipient's inbox before another send. An explicit `KIM_BRIDGE_UNCONFIGURED` response confirms that no send was attempted; it remains queued with backoff without an attempt limit. Lease-token checks fence all settlement and release writes.

### Printing [#printing]

The device decrypts the stored PDF in memory. Native builds pass a PDF data URI to the Expo print dialog; web uses a short-lived blob URL. The native module is patched and built from source so PDF bytes cannot become the print job name or an error message. A runtime marker refuses printing with an older or unpatched native binary. The operating system manages its own print spool. A `report.print_requested` trail event records the dialog handoff: Android does not report whether the user actually printed or cancelled.

### Why not route KIM through a shared email package [#why-not-route-kim-through-a-shared-email-package]

`packages/email` is the Mailchimp-driven marketing / transactional path (ticket 76). KIM is a different lifecycle:

- Credentials are per-organization and KEK-wrapped, not a single global secret.
- Delivery writes `delivery_event` + a `report.delivered` trail event + schedules a `report_no_reply` reminder — none of which `packages/email` knows about.
- The job leases through `kim_outbox` and respects `clinicalJobAccess` ("write"); emails do not.

A shared package would collapse these semantics into one surface and surface the KIM state in the wrong feature. The ticket's acceptance criteria name the KIM lifecycle fields directly.

## Consequences [#consequences]

- Adding a KIM provider is a single file in `packages/api/src/platform/kim/` + a two-line change in `index.ts`. The job, the trail receipt and the reminder stay the same.
- The device never holds KIM credentials and never speaks SMTP. The transport is strictly a server-side concern.
- Credentials never cross the API boundary: `kimConfig.get` reports presence, `kimConfig.set` wraps and stores, `kimTest` runs the transport without echoing the credentials.
- `/dev/kim-inbox` is a dev-only route: the backend returns 404 unless `KIM_TRANSPORT=fake` and the Vite build strips the component chunk out of a production bundle (same shape as `/dev/enrol`, ticket 29).
- The bridge is still [TBD]; the production transport reports `KIM_BRIDGE_UNCONFIGURED` and the row stays queued. This is intentional — the acceptance of a KIM send against a real bridge is deferred to ticket 78.

## Alternatives considered [#alternatives-considered]

- **Direct SMTP from a Worker** — rejected: no raw outbound SMTP from the current runtime without a bridge.
- **A per-tenant HTTP KIM gateway** — rejected for v1: none of the surveyed providers expose one; adding our own gateway duplicates the bridge work we already need.
- **Vendor email library embedded on the device** — rejected on principle: credentials never belong on the device, and signed-sender SMC-B is a server identity.

