Skip to content
NeuralRepo
Get Support

Security & Auth

NeuralRepo is designed with security at every layer. This page covers authentication, authorization, encryption, and data isolation.

NeuralRepo supports four sign-in methods. The three federated ones are implemented with the Arctic OAuth library:

ProviderProtocolImplementation
GitHubOAuth 2.0Arctic
GoogleOpenID ConnectArctic
AppleSign in with AppleArctic
Magic LinkEmail-basedCustom

The auth_accounts table records which providers are linked to an account, with provider one of github, google, apple, or email.

  1. User clicks “Sign in with GitHub” (or Google, or Apple).
  2. The server generates a state parameter and redirects to the provider.
  3. The provider authenticates the user and redirects back with an authorization code.
  4. The server exchanges the code for an access token and retrieves the user profile.
  5. If the email matches an existing user, the account is linked. Otherwise, a new user is created.
  6. A session is created and returned as an HttpOnly cookie.

For users who prefer email-based auth:

  1. User enters their email address.
  2. The server generates a one-time token, stores its SHA-256 hash in the magic_links table, and sends the token via email.
  3. The user clicks the link, which contains the token.
  4. The server hashes the token, looks up a record that is unexpired (15 minutes) and unused, and creates a session.
  5. The record is marked used with a used_at timestamp, not deleted — a replayed link finds the row and is rejected.

Sessions are the primary authentication mechanism for the web app.

PropertyValue
Token format32 random bytes, hex-encoded (64 characters)
Cookie namenrepo_session
StorageSHA-256 hash stored in D1 sessions table
Expiry30 days
Cookie flagsHttpOnly, Secure, SameSite=Lax, Path=/, Max-Age=2592000
Auto-refreshRolling — every authenticated request pushes expiry back to 30 days from now

The session is a rolling 30 days rather than a fixed one: any authenticated request extends it, so an account in daily use never expires, and one that goes quiet for a month does. Requests carrying the session as a Bearer token are refreshed in the database; requests carrying the cookie also get a fresh Set-Cookie.

The raw session token is only ever sent to the client as a cookie. The server stores and compares only the SHA-256 hash. This means a database breach does not expose usable session tokens.

API keys provide programmatic access for the CLI, Siri Shortcuts, CI/CD, and custom integrations.

PropertyValue
Formatnrp_ prefix + 32 random bytes hex-encoded — 68 characters in total
StorageSHA-256 hash stored in D1 api_keys table
DisplayNever shown again; the settings list identifies keys by label and the last 4 characters of the key record’s id
ScopesNULL on keys you create — full access. Scopes only exist on MCP OAuth tokens.
RevocationImmediate — the key record is deleted, and nothing is cached

When a request includes an X-API-Key header:

  1. The header is ignored unless the value starts with nrp_.
  2. The server hashes the provided key with SHA-256.
  3. It looks up the hash in the api_keys table.
  4. If found, the request is authenticated as the key’s owner, with the key’s scopes.
  5. The last_used_at timestamp is updated.

Credentials are checked in a fixed order: Authorization: Bearer (tried as a session token first, then as an API key or MCP token), then X-API-Key, then the session cookie. MCP OAuth tokens are not nrp_-prefixed, so they must be sent as BearerX-API-Key will not see them.

The full key is shown only once at creation time. It cannot be retrieved later.

User-provided AI keys (Anthropic, OpenAI, OpenRouter) are encrypted before storage:

PropertyValue
AlgorithmAES-256-GCM (Web Crypto)
Key derivationHKDF-SHA256 over the ENCRYPTION_KEY Workers secret, with a fixed salt and info string
IVFresh random 12-byte IV per encryption
StorageA single base64 string of IV ‖ ciphertext ‖ auth tag in D1

The secret is never used as an AES key directly — it is stretched through HKDF first, so the stored ciphertext does not depend on the secret’s length or entropy distribution. The encryption key is a Workers secret that never appears in code or logs. Decryption happens in-memory only when making an AI request, and the plaintext key is never written to disk or returned via API.

The MCP integration uses a full OAuth 2.0 with PKCE flow to authorize Claude’s access:

PropertyValue
Grant typeAuthorization Code with PKCE, plus refresh_token
Challenge methodS256 (SHA-256) or plain
Client registrationDynamic Client Registration (RFC 7591) at /mcp/register, kept 30 days
Authorization code expiry10 minutes, single use
Access token expiry30 days
Refresh token expiry90 days, rotated on every refresh
Scopesideas:read, ideas:write

The PKCE flow prevents authorization code interception attacks. The MCP client generates a random code_verifier, hashes it to create a code_challenge, and sends the challenge with the authorization request. When exchanging the code for tokens, the server verifies the original verifier matches the stored challenge.

Authorization codes and refresh tokens are both stored as SHA-256 hashes, in oauth_authorization_codes and oauth_refresh_tokens. Scopes are enforced at the tool boundary: every MCP tool that writes requires ideas:write, and a token carrying neither scope is refused.

The API uses strict CORS configuration:

HeaderValue
Access-Control-Allow-OriginThe request origin if it is https://neuralrepo.com or any *.neuralrepo.com subdomain; otherwise APP_URL
Access-Control-Allow-MethodsGET, POST, PATCH, DELETE, OPTIONS
Access-Control-Allow-HeadersContent-Type, Authorization, X-API-Key
Access-Control-Allow-Credentialstrue
Access-Control-Max-Age86400 (24 hours)

Reflection is limited to NeuralRepo’s own domains — an unrecognized origin is answered with the app URL rather than itself, so the browser blocks it. Non-production environments additionally allow *.workers.dev (staging) and http://localhost:*.

Rate limits are enforced using Cloudflare KV as a daily counter store. Limits apply only to requests authenticated via API key (X-API-Key header) or Bearer token. Web UI sessions are exempt.

PlanDaily Limit
Free100 requests/day
Pro10,000 requests/day

When a limit is exceeded, the API returns 429 Too Many Requests — with no Retry-After header.

The counter key format is rate:{user_id}:{YYYY-MM-DD} with a 24-hour TTL. The date is UTC, so counters reset at UTC midnight. The limit is per user, not per key: every API key, MCP client, and CLI install on one account shares the same budget.

Every user-facing database query in NeuralRepo is scoped by user_id:

SELECT * FROM ideas WHERE user_id = ? AND is_archived = 0

Each user’s data is isolated — you cannot access, search, or modify another user’s ideas regardless of the authentication method. Ownership is checked on the read, not just at the route: fetching an idea by id requires the id and the user id to match, so guessing an id gets you a 404.

Every response carries a Content-Security-Policy, set by middleware that rebuilds the response so the header lands even on immutable responses passed through from the static-assets binding. The Worker is deliberately configured to run before asset serving for exactly this reason — otherwise the SPA’s own HTML would be returned without a CSP.

DirectiveValue
default-src'self'
script-src'self' — no inline scripts, no CDNs
style-src'self' 'unsafe-inline' plus Google Fonts
font-src'self' plus Google Fonts
img-src'self', data:, blob:, and the Google/GitHub avatar hosts
connect-src'self' plus Stripe
frame-src'self' plus Stripe (checkout)
object-src'none'
base-uri'self'