Legal

Privacy Policy

Last updated: 2026-07-06

Noesa is a time-tracking and activity-attribution app whose privacy properties are enforced by its code, not merely promised. This policy states what we collect. More importantly, it states what we cannot collect by construction. Every substantive claim below cites the file and symbol that enforces it, so you (or an auditor) can verify it. The Worker source is open (MIT) and the complete network inventory is published in network-activity.

Data controllers: Moritz Wallawitsch and Arne Strickmann (see repository LICENSE).
Contact: owner to supply a published privacy contact address before release.

The short version

  • No account. There is no sign-up, email, or password.
  • Your screen never leaves your device. Perception is on-device OCR; there is no vision model and no endpoint that accepts an image.
  • We can't read your prompts. The proxy streams request bodies straight to the AI provider and never reads, buffers, or logs them.
  • Analytics and diagnostics are opt-in, are aggregate counters of whitelisted fields, contain no content, and auto-expire.
  • You can avoid our servers entirely via Bring-Your-Own-Key or fully local/offline mode.

Account-less, pseudonymous identity

Noesa has no user accounts. The backend classifies a caller only as free or licensed (authenticate, worker/src/auth.ts):

  • Free tier is identified by X-Noesa-Device, a random 64-hex identifier generated once on your device by the OS random-number generator and stored locally (desktop/app/Sources/NoesaKit/AppCore.swift; validated as 64 hex chars by DEVICE_RE in worker/src/auth.ts). It is not derived from your hardware and is not a hash of anything. It is an opaque, per-install pseudonym with no personal data in it. Reinstalling generates a fresh one (which also resets the free-tier meter).
  • Paid tier is identified by a license key.

The device id is used only as an opaque bucket key for free-tier rate-limiting and daily metering. Analytics stores no device identifier at all (see below), so there is no shared key that could link your free-tier/billing identity to your analytics. There is nothing to correlate.

Your screen stays on your device

Perception in Noesa is on-device OCR plus a deterministic idle/active tag: there is no vision model, cloud or local (docs/goals/native-ui-trust-backend-decisions.md, "Vision model already removed"; .env.example notes per-frame perception is model-free). Consequently:

  • No screenshot or pixel data is ever transmitted anywhere. This is stronger than "images don't leave": nothing off-device consumes them, so there is nothing to leak.
  • No endpoint accepts an image. The one endpoint that transmits your content is the LLM proxy route, whose payload is derived text; the allowed model set (MODEL_ALLOWLIST, worker/src/config.ts) is a text model only.
  • Screenshots are used solely for on-device OCR and local evidence replay.

What actually leaves your device (only when you opt in)

The only content that egresses is derived text, and only when you use cloud AI (session synthesis or activity attribution). That text is sent to POST /api/llm/v1/chat/completions (handleLlm, worker/src/proxy.ts), which is a key-custody passthrough with these guarantees:

  • Bodies are never read, buffered, or logged. handleLlm passes body: req.body straight to the upstream fetch with duplex: "half"; the model/feature are declared in headers (X-Noesa-Model/X-Noesa-Feature) so no code needs to inspect the payload (worker/src/proxy.ts).
  • Your identity is stripped before the provider sees the request. buildUpstreamHeaders (worker/src/headers.ts) builds the upstream headers from an allowlist (content-type + the server key + a static x-title), so any Authorization, Cookie, Host, X-Forwarded-For, X-Real-IP, CF-Connecting-IP, or X-Noesa-* header cannot reach the provider.
  • The provider key is ours, server-side only. It is injected from the OPENROUTER_API_KEY secret (worker/src/env.ts) and never returned to the client. Responses pass through only content-type/content-length (RESPONSE_HEADERS_ALLOW, worker/src/config.ts; filterResponseHeaders, worker/src/headers.ts).
  • Metering counts tokens, not content. meterUsage (worker/src/meter.ts) forwards every byte to you unbuffered and only scans a bounded tail for the numeric usage block; content is never parsed.
  • Logs are metadata only. LogRecord (worker/src/log.ts) has no field for a body, prompt, header, or query; it records model, feature, auth kind, an 8-char id prefix, status, elapsed ms, and an optional integer token count. The error handler logs only an error class name (worker/src/index.ts).

Opt-in analytics and diagnostics

Both are off unless you opt in, and both store only aggregate counters of whitelisted fields:

  • Analytics (POST /api/analytics, handleAnalytics, worker/src/endpoints.ts): reads only an event name, truncates it to 64 characters, and increments a per-UTC-day counter (analytics:<day>:<event>). No content, no free-form fields. (The current handler stores only the event count and does not persist a device id.)
  • Diagnostics (POST /api/diagnostics, handleDiagnostics, worker/src/endpoints.ts): reads only an errorClass and code, each truncated to 64 characters, and increments a per-UTC-day counter. No stack traces, no messages, no content, no user data.

Retention. These counters auto-expire via a Cloudflare KV TTL of 60 × 60 × 24 × 90 seconds (90 days), set as expirationTtl in both handlers (worker/src/endpoints.ts). After that they are deleted automatically.

Free-tier metering and licensing

  • The free daily token cap (FREE_TIER.dailyTokenBudget, worker/src/config.ts) is enforced per device id per server UTC day by the FreeTierBudget Durable Object (worker/src/budget.ts; utcDay() in worker/src/proxy.ts). It stores an integer token count per day and no content.
  • License validation (POST /api/license/validate, handleLicenseValidate, worker/src/endpoints.ts) returns cached { valid, plan, periodEnd } and never returns secrets. Paid usage reporting (POST /api/usage/report) stores integer token counts per feature only.
  • Billing is handled by a signed provider webhook (POST /api/billing/webhook, handleBillingWebhook, worker/src/webhook.ts), verified with a constant-time HMAC over the raw body. Entitlement is status-driven from the Lemon Squeezy subscription lifecycle; the plan-name map and license-key source are store-config-dependent, and the Stripe-vs-Lemon-Squeezy provider choice is still open (docs/goals/native-ui-trust-backend-decisions.md, §9), so billing is not production-ready until that is confirmed.

What we do NOT collect

Matching the published network inventory:

  • No screenshots or pixel data: no endpoint accepts an image (worker/src/config.ts MODEL_ALLOWLIST is text-only; perception is on-device).
  • No prompt or response content: bodies are streamed, never read/logged (worker/src/proxy.ts, worker/src/log.ts).
  • No hardware identifiers: the free-tier id is a random per-install token, not derived from your device (desktop/app/Sources/NoesaKit/AppCore.swift, worker/src/auth.ts).
  • No account data: there are no accounts.
  • No IP-linked identity forwarded to the provider: CF-Connecting-IP/X-Forwarded-For are stripped upstream (buildUpstreamHeaders, worker/src/headers.ts). (IPs are used transiently, in-memory, for best-effort per-minute rate-limit buckets keyed by IP; worker/src/ratelimit.ts.)
  • No stack traces, error messages, or free-form diagnostic text (worker/src/endpoints.ts, worker/src/index.ts).
  • No cross-identity correlation key: analytics stores no device identifier at all, so your free-tier/billing identity cannot be linked to analytics (worker/src/endpoints.ts).

Data retention summary

DataWhereRetention
Aggregate analytics/diagnostics counters Cloudflare KV 90 days (auto-expiring TTL, worker/src/endpoints.ts)
Free-tier per-device daily token count Durable Object Per server UTC day (worker/src/budget.ts)
Per-minute rate-limit buckets Cloudflare KV 120 s TTL (worker/src/ratelimit.ts)
License cache Cloudflare KV Until updated by the billing webhook (worker/src/webhook.ts)
Prompt/response content None Never stored (worker/src/proxy.ts, worker/src/log.ts)

Release integrity

Release builds are intended to be code-signed and notarized (macOS Developer ID + Apple notarization; Windows code-signing) so you can verify they are unmodified. These are ship-gating, owner-credential steps tracked in docs/goals/native-ui-trust-backend-decisions.md ("Owner-held prerequisites").

Your rights (GDPR / CCPA)

Because Noesa has no accounts and stores no content, there is very little personal data to act on: your activity data lives locally on your device and analytics/diagnostics are anonymous aggregate counters that expire automatically.

  • Access / portability: your substantive data is the local activity database on your machine; you already hold it.
  • Erasure: uninstalling removes local data; server-side aggregate counters expire within the retention window above; contact us to remove a license record tied to a purchase.
  • Objection / opt-out: analytics and diagnostics are opt-in. Leave them off, or use BYOK/Local mode so nothing reaches our servers at all.
  • Non-discrimination (CCPA): declining analytics does not degrade the product.

For EU/UK users, our lawful basis for the minimal aggregate telemetry is your consent (opt-in), and for licensing/rate-limiting it is performance of the service you requested. To exercise any right, use the privacy contact above.

Changes

We may update this policy; material changes will be noted in the changelog and this file's "Last updated" date.