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 byDEVICE_REinworker/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.
handleLlmpassesbody: req.bodystraight to the upstreamfetchwithduplex: "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 staticx-title), so anyAuthorization,Cookie,Host,X-Forwarded-For,X-Real-IP,CF-Connecting-IP, orX-Noesa-*header cannot reach the provider. - The provider key is ours, server-side only. It is injected from the
OPENROUTER_API_KEYsecret (worker/src/env.ts) and never returned to the client. Responses pass through onlycontent-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 numericusageblock; 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 aneventname, 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 anerrorClassandcode, 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 theFreeTierBudgetDurable Object (worker/src/budget.ts;utcDay()inworker/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.tsMODEL_ALLOWLISTis 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-Forare 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
| Data | Where | Retention |
|---|---|---|
| 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.