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.
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).
Kopra uses three distinct authentication mechanisms, each designed for a specific integration pattern.
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.
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.
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.
Embed tokens have several safeguards beyond basic JWT signing:
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.
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.
Kopra runs on Hetzner Cloud in Europe, with all data stored in European data centers. The infrastructure stack:
X-Forwarded-For, not the proxy's addressTransparency matters. Here is what is not yet in place:
These are on the roadmap. If any of them are blockers for your use case, reach out and we can discuss timelines.
Before going live with Kopra in your production environment, verify each of these:
KOPRA_CORS_ORIGIN environment variable in production. Do not leave it as the default wildcard.If you have questions about any of these security layers or need details for a vendor security questionnaire, contact us at support@kopra.dev.