[ Security ]
Every answer can defend itself.
So can the platform.
Norami handles operational data — financial projections, production numbers, cell-by-cell workbooks. We treat it the way an auditor would: identity verified, tenant-scoped at the database, encrypted in transit and at rest, and traceable to a specific query and a specific row in an append-only ledger.
- TOTP 2FA
- Row-Level Security
- AES-256 at rest
- TLS 1.2+
- Append-only audit
- BYOK LLM keys
[ Identity & Access ]
Prove who you are.
Every session, every request.
Authentication
Email and password, with passwords hashed by the auth provider — Norami’s application never sees them. Sessions are issued as short-lived JWTs stored in httpOnly cookies with Secure and SameSite set, and re-validated server-side on every protected request — never just trusted from the cookie.
Two-factor authentication
TOTP-based 2FA via standard authenticator apps. Once a factor is enrolled, a password-only session (NIST AAL1) cannot reach any protected route — middleware enforces step-up to AAL2 before rendering a single page. Disabling 2FA requires a fresh code from the enrolled device, so a stolen password alone cannot undo it.
Forced password rotation
Generic passwords issued by an admin during onboarding carry a must_change_password flag. Until cleared, the user is redirected to a rotation page on every navigation. There is no other way through the app.
Privileged access
Platform-admin access is a separate authorization plane, gated on every admin route by a database-side check. Application role and database role are evaluated independently — neither grants the other.
[ Authorization ]
Every request knows who,
where, and what.
Authentication tells the system who you are. Authorization tells it what you’re allowed to reach inside your tenant. Both are evaluated server-side on every protected request, and the database re-checks them through Row-Level Security. The browser never carries a permission claim the server believes on its own.
Identity propagation
Every authenticated request carries the user’s id, their tenant_id, and their granted access set. The session JWT identifies the user; tenant membership and access scope are looked up server-side and never trusted from the client.
Per-bot access grants
Inside a tenant, users only see the bots their admin has explicitly granted access to. Grants live in user_bot_permissions, are checked on every chat and query route, and take effect on the next request after revocation.
Dataset scope
Each bot is bound to a specific set of datasets. Queries it generates can only reach those — the dataset ids are parameter-passed into the database-side validator, then re-checked by RLS on the underlying tables.
[ Isolation ]
Tenant boundaries
enforced by the database itself.
Every table that holds customer data carries a tenant_id. Every read and every write is checked against that tenant_id through Row-Level Security policies — enforced by Postgres on every query, regardless of the application path that issued it. A bug in the API does not collapse the tenant boundary.
RLS, not application checks
Tenant policies live next to the data, not in the API. Bypassing them requires bypassing the database engine.
User-scoped chat
Chat sessions are scoped to the user, not the tenant. Even within a tenant, you only see your own conversations. Messages inherit visibility from session ownership — there is no read by guessing a session id.
Service role is sealed
The privileged service role bypasses RLS by design. It is reachable only from a tightly scoped allowlist of server actions. It is never available in the browser.
[ Encryption ]
In transit.
And at rest.
In transit
TLS 1.2 or higher on every connection — application, database, and outbound calls to sub-processors. HSTS preload with a 2-year max-age: once a browser has loaded the site over HTTPS, plain HTTP is refused thereafter.
At rest
AES-256 across the Postgres database and object storage. Customer-supplied LLM API keys (BYOK) are stored in the same encrypted store and only readable inside the privileged service-role context — never returned to the client.
[ Audit ]
An append-only ledger.
Built into the database.
Every authentication event, every administrative mutation, every data export, every security signal is written to audit_logs with the actor, the resource, the before/after diff, the IP address, the user-agent, and the timestamp.
Each row links to the previous row via a SHA-256 hash chain. The content hash of a row covers its fields plus the prior row’s hash, so altering any past entry invalidates every entry that follows. A verification routine walks the chain and reports the first inconsistency it finds.
UPDATE and DELETE on the audit table are blocked by triggers — for everyone, including the privileged service role. The ledger is append-only at the engine level, not by application convention.
audit_logs (
id,
tenant_id, user_id, actor_email,
event_action, -- e.g. auth.mfa_enrolled
resource_type, resource_id,
metadata, -- jsonb diff
ip_address, user_agent,
created_at,
prev_hash, -- previous row's hash
content_hash -- sha256(row + prev_hash)
)
trigger trg_audit_logs_no_update
-> RAISE 'audit_logs is append-only'
trigger trg_audit_logs_no_delete
-> RAISE 'audit_logs is append-only'[ Application ]
Hardened where it counts.
By design, not by review.
Atomic rate limiting
Per-user, per-action limits live in the database, with advisory transaction locks serializing the count-then-insert. Two concurrent requests cannot both pass the check before either reservation lands.
SQL injection, closed at the database
Analyst queries generated by the LLM never reach customer tables directly. They execute inside a database-layer validator that allows only single SELECT/WITH statements; blocks DML, DDL, comments, statement chaining, set operations, dollar-quoted strings, and implicit cross joins; and restricts every FROM/JOIN to two pre-scoped views. Those views are parameter-bound to the caller’s tenant_id and granted dataset ids — never string-interpolated. Statement timeout 8 s; row cap 200.
Server-side enforcement
Every protected route validates identity, AAL level, password-rotation flag, and admin status server-side on each request. The browser is never trusted to enforce its own gates.
Privilege boundary
Database functions that escalate beyond the caller’s row-level access are an explicit allowlist with bounded behavior. Everything else runs with caller permissions.
Secrets discipline
Application secrets are server-only. Customer-supplied LLM keys are encrypted at rest and only decrypted inside the privileged service-role context that issues the LLM call.
No-train default
Customer Data is never used to train Norami’s models. Our LLM providers operate under their default API terms — which exclude API traffic from training. BYOK shifts this entirely under the customer’s contract.
[ Foundation ]
Built on managed cloud.
Customer data — uploaded files, the parsed cell-by-cell index, every answer, every audit entry — lives in managed Postgres and object storage running on AWS. The infrastructure tier inherits AWS’s SOC 2 Type II and ISO 27001 attestations; the controls described above sit on top of that foundation.
[ Sub-processors ]
Where data flows.
And why.
Operating Norami involves routing some customer data through a small number of vetted services. Each is listed here with what it receives and what it’s for. We update this page when we add or remove a sub-processor.
| Service | Category | Receives | Purpose |
|---|---|---|---|
| Supabase | Database, auth, storage | All customer data | Primary data store, identity, and 2FA |
| OpenAI | LLM inference | Chat content (unless BYOK) | Generates answers to your questions |
| Anthropic | LLM inference | Chat content (unless BYOK) | Generates answers to your questions |
| Perplexity | Web research | Public-domain query only | Initial tenant-setup research (public-profile lookup) |
| Resend | User email + name | Password reset and transactional email | |
| E2B | Code sandbox | Query + dataset snippets | Executes Python/JS transforms in an isolated container |
No sub-processor trains on your data.
Norami does not use Customer Data to train models. Our LLM providers operate under their default API terms: OpenAI has not used API traffic for model training since March 2023, and Anthropic applies the same policy to API traffic. When BYOK is active, requests route through the customer’s account and are governed by the customer’s contract with the LLM provider.
[ Not yet ]
What we haven’t shipped.
And what’s next.
Being honest saves everyone a week of discovery calls. Here is the truth about the items a security review will ask about and we have not yet shipped.
SOC 2 (Type I / Type II)
Not yet certified. We are building the control documentation against the trust services criteria. The underlying infrastructure tier is already SOC 2 Type II attested.
SSO / SAML
Not available today. Planned. Customers who require it for procurement will hear when it ships.
Formal penetration test
Not yet conducted. We will commission a third-party test alongside the SOC 2 audit. In the meantime, see Responsible Disclosure below.
Automated dependency scanning
Not in place yet. Security advisories are tracked manually against our dependency set.
Malware scanning on uploads
Uploads are MIME-type validated, size-bounded, and rate-limited. They are not yet scanned for malicious payload content.
Formal RTO / RPO
Point-in-time recovery is enabled at the database layer. We have not yet published a formal recovery-time and recovery-point objective.
[ Disclosure ]
Found something?
Tell us.
We read every security report. If you think you’ve found a vulnerability in Norami or the marketing site, email security@norami.ai. Please include enough detail that we can reproduce the issue. We commit to acknowledging reports within 72 hours. We don’t currently run a paid bug bounty.
Last updated: 2026-04-29.