← Back to blog

How Kopra Secures Your Customer Data

April 1, 2026

When you embed Kopra into your product, your customers' data flows through our infrastructure. That means security is not a feature we bolt on later. It is built into every layer: data access, authentication, token generation, API endpoints, webhook delivery, and hosting.

This post walks through each security layer in detail, so you can evaluate whether Kopra meets your production requirements.

1. Multi-Tenant Data Isolation

Every row in Kopra's database carries a clientId (your account) and, where applicable, a tenantId (your customer). These identifiers are not optional filters. They are mandatory parameters on every database query.

The KopraRepository class enforces this at the data access layer. When fetching field schemas, saving field values, or querying any tenant-scoped data, both clientId and tenantId are included in the Prisma where clause. There is no code path that retrieves data without tenant scoping.

// Every query includes clientId - there is no "get all" without scoping
const fieldGroup = await prisma.fieldGroup.findFirst({
  where: { id: fieldsGroupId, clientId, deletedAt: null }
});

Soft deletes add another layer. Deleted records are never returned in queries because every where clause includes deletedAt: null (enforced via a shared notDeleted filter). This prevents accidentally exposing data that a customer has deleted.

The isolation model means that even if a bug exists in one query, cross-tenant data leakage requires bypassing both the authentication middleware (which sets clientId from a verified credential) and the repository layer (which requires it on every call).

2. Three Authentication Methods

Kopra uses three distinct authentication mechanisms, each designed for a specific integration pattern.

Session Authentication (Dashboard)

Logged-in users interact with Kopra's management dashboard through Wasp's session-based auth. Sessions are HTTP-only cookies with standard CSRF protections. This is the only method that provides access to account management, billing, and field group configuration.

JWT Tokens (Embeds)

When you embed Kopra's field editor into your product via an iframe, authentication happens through short-lived JWT tokens. Your backend requests a token from Kopra's API, scoped to a specific clientId and tenantId. The token is then passed to the iframe, which uses it for all subsequent requests.

The token payload contains exactly the claims needed for isolation:

interface EmbedTokenPayload {
  clientId: string;   // Your Kopra account
  tenantId: string;   // Your customer
  fieldLimit?: number; // Optional cap on fields
  iat: number;        // Issued at
  exp: number;        // Expiration
}

Tokens are signed with HS256 and verified on every request. If a token is missing clientId or tenantId, the request is rejected before any data access occurs.

API Keys (Server-to-Server)

For REST API integrations, Kopra uses API keys passed via the X-API-Key header. Keys are generated with 24 bytes of cryptographic randomness and prefixed with kp_live_ for easy identification. Only the SHA-256 hash is stored in the database. The raw key is shown once at creation and never stored or logged.

function hashApiKey(key: string): string {
  return crypto.createHash('sha256').update(key).digest('hex');
}

On each request, the middleware hashes the provided key and looks up the hash. This means that even a full database breach does not expose usable API keys.

3. Token Security

Embed tokens have several safeguards beyond basic JWT signing:

  • Configurable expiry with a hard maximum of 7 days. The default is 1 hour. Supported units: seconds, minutes, hours, days, and weeks. Any value exceeding the 7-day cap is silently clamped.
  • Aggressive rate limiting on the token endpoint: 50 requests per 15 minutes per IP. This prevents brute-force token generation and limits the blast radius of a compromised API key.
  • Minimal claims: tokens contain only the identifiers needed for data scoping. No personally identifiable information, no permissions beyond tenant scope.
  • Algorithm restriction: token verification explicitly requires HS256, preventing algorithm confusion attacks.

4. API Security

All API routes pass through layered middleware before reaching any business logic.

Rate limiting is applied globally at 1,000 requests per 15-minute window. Authentication endpoints have stricter limits: 50 requests per 15 minutes for token generation, 10 for login and signup, and 5 for password reset. Rate limit headers follow the standard RateLimit-* format so clients can implement backoff.

Input validation uses Zod schemas for all request bodies. Field definitions, field values, and configuration payloads are parsed and validated before they reach the database layer. Invalid input returns structured error responses with specific field-level messages.

Error handling follows HTTP semantics consistently: 400 for validation errors, 401 for missing or invalid credentials, 403 for insufficient permissions or inactive subscriptions, 404 for missing resources (with tenant scoping, so you cannot probe for resources in other tenants), and 429 for rate limit violations.

Request logging records the method, path, status code, and response time for every API call, enabling anomaly detection and incident investigation.

5. Webhook Security

When Kopra dispatches webhook events (on field group, field, or field value changes), each delivery is signed with HMAC-SHA256 using a per-endpoint secret.

function computeSignature(secret: string, payload: Record<string, any>): string {
  return crypto.createHmac('sha256', secret)
    .update(JSON.stringify(payload))
    .digest('hex');
}

The signature is sent in the X-Kopra-Signature header. Your webhook handler should compute the same HMAC over the raw request body and compare it in constant time to verify authenticity.

Each webhook endpoint has its own secret, generated at creation. If one endpoint's secret is compromised, other endpoints remain secure.

Delivery tracking records the status code, response, and attempt count for every delivery. Failed deliveries retry with exponential backoff: 1 minute, 5 minutes, 30 minutes, 2 hours, and 12 hours. After 5 failed attempts, the delivery is marked as permanently failed. A background job (powered by PgBoss) checks for pending retries every minute.

Webhook requests have a 30-second timeout to prevent slow endpoints from blocking the delivery pipeline.

6. Infrastructure

Kopra runs on Hetzner Cloud in Europe, with all data stored in European data centers. The infrastructure stack:

  • TLS encryption for all traffic, terminated at the Caddy reverse proxy with automatic certificate management via Let's Encrypt
  • Docker containers for application isolation, with separate containers for the web server, database, and background job runner
  • PostgreSQL as the sole data store, accessed through Prisma ORM with parameterized queries (no raw SQL string interpolation)
  • Caddy as the reverse proxy, providing automatic HTTPS, HTTP/2, and request routing
  • Trust proxy configuration ensures rate limiting uses the real client IP from X-Forwarded-For, not the proxy's address

7. What Kopra Does Not Yet Have

Transparency matters. Here is what is not yet in place:

  • SOC 2 certification: Kopra has not undergone a SOC 2 audit. If your compliance team requires this, we are not there yet.
  • GDPR Data Processing Agreement (DPA): A formal DPA is not currently available. Kopra stores data in Europe and does not transfer it outside the EU, but we do not yet offer a signed DPA.
  • Single-tenant deployment: All customers share the same infrastructure. Dedicated instances are not available today.
  • Encryption at rest: PostgreSQL data is stored on encrypted volumes at the infrastructure level (Hetzner's default), but Kopra does not implement application-level encryption for field values.
  • Audit log export: Audit logs are stored internally and visible in the dashboard, but there is no API endpoint for exporting them to your SIEM.

These are on the roadmap. If any of them are blockers for your use case, reach out and we can discuss timelines.

Security Checklist for Production

Before going live with Kopra in your production environment, verify each of these:

  • Generate a strong JWT_SECRET (at least 32 bytes of randomness) and store it in your environment variables, not in code
  • Keep API keys server-side only. Never expose them in client-side JavaScript, mobile apps, or public repositories
  • Set short token expiry times. Use the minimum duration your UX requires. One hour is the default for a reason.
  • Verify webhook signatures in your handler using constant-time comparison. Do not skip this step even in development.
  • Rotate API keys periodically. Kopra supports multiple active keys, so you can create a new key before deactivating the old one.
  • Configure CORS origins via the KOPRA_CORS_ORIGIN environment variable in production. Do not leave it as the default wildcard.
  • Monitor rate limit headers in your API client. Implement exponential backoff when you receive 429 responses.
  • Review the audit log in the Kopra dashboard regularly for unexpected actions or access patterns.
  • Use HTTPS for webhook endpoints. Kopra will deliver to HTTP URLs, but the payload travels in plaintext without TLS.
  • Test token expiry handling in your embed integration. When a token expires mid-session, your app should request a new one gracefully.

If you have questions about any of these security layers or need details for a vendor security questionnaire, contact us at support@kopra.dev.