Tenant isolation you can verify.
Framique runs on self-hosted Postgres with row-level security on every tenant table, scoped and rotatable API keys, and an audit trail you can export. This page explains the mechanisms, not just the promises.
request → session → membership → policy → row
The tenancy isolation model
The database says no before the row leaves storage.
For a non-engineer
Every merchant's orders, customers, products and payouts live in the same physical database as every other merchant's — that is normal, efficient, and how almost every serious SaaS platform works. What matters is what stops one merchant's application code, one buggy report, or one careless staff query from ever returning a row that belongs to someone else.
We do not rely on our own code remembering to add a filter to every query, on every route, forever, across every engineer who ever touches the codebase. That approach fails eventually — not because engineers are careless, but because “remember to add a filter every time” is a rule a human will eventually forget under deadline pressure, and forgetting it once is a data breach.
Instead, the database itself refuses to return a row unless the request is provably allowed to see it. The check lives in Postgres, underneath the application, so even a route that has a bug, a report that joins tables incorrectly, or a new hire's first pull request cannot leak another merchant's data. This is called row-level security (RLS), and it is turned on for every table that holds merchant data. A table with no isolation policy returns nothing to anyone — a loud failure we notice immediately, not a silent leak we discover later.
For an engineer
- Every tenant-scoped table carries a merchant_id column and an ENABLE ROW LEVEL SECURITY policy. There is no opt-out table for tenant-scoped “internal” data.
- Policies call security-definer helper functions — is_merchant_member(), has_merchant_role(), staff_has(), is_platform_admin() — rather than inlining logic per policy, so a fix or audit touches one function, not forty policies.
- Roles never live on the profiles row. Entitlement lives in a separate merchant_members(user_id, merchant_id, role) table, so revoking access is a delete on a membership row — auditable, reversible, and impossible to confuse with “the user no longer exists.”
- The request path is uniform for every actor, including our own staff: request → session identity (GoTrue JWT) → merchant membership lookup → row-level policy evaluation → row. There is no back-door service-role connection wired into a dashboard.
- Isolation is a test suite, not a design intention: a negative-assertion end-to-end spec runs on every release — logged in as merchant A, every attempt to read, list, update or delete a row belonging to merchant B must fail.
- GRANTs are explicit per role in addition to policies. A table with RLS enabled but no grant for a role is unreachable by that role at the connection layer, before policy evaluation even runs.
Request path
- Request
- Session
- Membership
- Policy
- Row
Every request walks this path, including our own staff tooling. The policy node is Postgres row-level security evaluating whether the caller may see the row at all — a check the application cannot bypass by forgetting a filter.
| Where each isolation control lives | Mechanism | Fails how, if misconfigured |
|---|---|---|
| Connection | Explicit GRANT per role, per table | No grant → connection-level denial, before RLS even evaluates |
| Row | ENABLE ROW LEVEL SECURITY + policy per table | No policy → zero rows returned to any role, a loud break not a leak |
| Policy logic | SECURITY DEFINER helper functions, not inlined per-policy SQL | Centralised, so a fix or audit touches one function, not forty policies |
| Identity vs entitlement | merchant_members(user_id, merchant_id, role) — never a column on profiles | Revocation is a row delete; escalation cannot happen through a profile update |
| Privileged operations | Service role loaded inside the handler, after caller verification, audit-logged | Never the default client; never reachable before authorization |
| Regression protection | Negative-assertion E2E suite as a release gate | A broken policy fails CI, not a customer's trust |
Authentication and session handling
Short-lived access, revocable refresh
Access tokens are short-lived; a stolen one has a narrow window before it expires and must be refreshed against a token that is itself revocable. Refresh tokens are rotated on use and stored in an httpOnly, Secure, SameSite=Lax cookie — never in localStorage.
Server-side session revocation
Sessions can be revoked server-side — password change, “log out everywhere,” suspected compromise, staff offboarding — without waiting for natural expiry.
No readable passwords
Passwords are hashed with a modern adaptive hash inside GoTrue; we never see or store a plaintext password, and support staff cannot look one up because there is nothing readable to look up.
Two-factor authentication
Optional TOTP is available for merchant accounts and required for platform-admin accounts — the small set of internal roles with cross-merchant visibility.
Membership re-derived, not cached
Every authenticated request re-derives merchant membership from the database on the request path, so a revoked member cannot keep acting on a stale token until it expires.
Edge rate limiting
Brute-force and credential-stuffing protection sits at the edge ahead of the auth service, keyed by IP and by account, with backoff rather than a hard lock a bad actor could use to lock out a legitimate merchant.
Roles and least-privilege permission matrix
Every staff member operating inside a merchant's account holds exactly one role per merchant, and every route/serverFn checks a specific permission via staff_has(permission) rather than a role name directly — so a permission can be re-assigned across roles without touching call sites.
| Roles and least-privilege permission matrix | Owner | Manager | Staff | Support (viewer) | Platform admin |
|---|---|---|---|---|---|
| View orders & customers | read-only, audited | audited, break-glass | |||
| Edit products & inventory | — not granted | — not granted | |||
| Issue refunds | limit-capped | — not granted | — not granted | ||
| Manage staff & roles | — not granted | — not granted | — not granted | — not granted | |
| View payout account details | masked | — not granted | — not granted | masked | |
| Rotate / revoke API keys | — not granted | — not granted | — not granted | ||
| Export customer data | — not granted | — not granted | audited, on request only | ||
| Delete store / close account | — not granted | — not granted | — not granted | — not granted | |
| Access billing & plan | — not granted | — not granted | — not granted | — not granted | |
| Cross-merchant visibility | — not granted | — not granted | — not granted | — not granted | audited, scoped, time-boxed |
- Masked payout details: a manager can confirm a payout method is on file and its last four digits, never the full account number.
- Refunds, limit-capped: staff can issue refunds up to a merchant-configured ceiling; anything above it requires a manager or owner.
- Platform-admin cross-merchant visibility is not a standing permission — it is granted per-incident, time-boxed, and every access is written to the audit log with the justifying ticket ID. There is no always-on “view any store” toggle.
API key scoping and rotation
API keys authenticate server-to-server integrations — custom apps, couriers, accounting exports — not end users. Each key is scoped to a single merchant; there is no platform-wide key that reaches across tenants.
Scope it
Watch it
Rotate it
Keys are stored hashed, not in reversible form, so a database compromise does not hand over usable keys — the same principle applied to passwords.
Secret handling — what we never log
Secrets — API keys, session tokens, payment credentials, service-role keys, webhook signing secrets — follow one rule: read inside the handler that needs them, never at module scope, never passed further than necessary.
- Never: a secret survives into a log line — scrubPayload / scrubText redact known secret-shaped values before a line leaves the process.
- Never: a secret appears in an error message returned to a browser, a Sentry error body, or a metric label.
- Never: a secret sits in a URL query string that could end up in access logs or browser history.
- Never: a secret is committed to the repository — environment secrets are read at runtime, and a pre-commit / CI secret-scan step exists to catch the case where someone tries anyway.
- Never: a client bundle ships a secret — bundles are scanned for accidental inclusion before release.
- Never: a merchant is asked for their password “to help debug.” Support access goes through the audited, time-boxed platform-admin path, not credential sharing.
Payment data handling
Payment data handling and tokenisation boundary
Framique supports cash on delivery, mobile financial services (bKash, Nagad, Rocket), bank transfer and BNPL, routed through an in-house payments aggregator with provider adapters behind one idempotent charge / refund / payout contract.
- Card and MFS credentials are handled by the payment processor's own hosted flow, not typed into a Framique-controlled form field — the sensitive credential never transits our application servers.
- What we store is a reference token, masked details (last four digits, method type) and transaction state — never enough to replay a charge or reconstruct the original credential.
- Idempotency is structural, not best-effort: charges, refunds and payouts are keyed through Redis/DB idempotency keys, so a network retry or a doubled webhook resolves to the original result rather than a second charge.
- Money is stored as currency_code plus integer minor units (paisa), never a float.
- Webhook deliveries are HMAC-signature verified before any privileged read or write happens on their contents.
- Refund authority is capped per role so a single compromised low-privilege account cannot issue unlimited refunds.
Roadmap: A formal PCI attestation is not something we hold today and this page does not claim one; our architecture keeps raw card data off our servers precisely so that scope stays as small as an eventual assessment would need it to be.
Encryption in transit and at rest
In transit
TLS terminates at the OpenResty edge with ACME-managed certificates auto-renewed ahead of expiry; internal service-to-service traffic runs inside a private network boundary, not exposed to the public internet.
At rest
Postgres volumes and storage buckets are encrypted at the disk layer; recoverable secrets and credentials use envelope encryption rather than a single static key baked into configuration.
Backups
Backups inherit the same at-rest encryption as the primary store — a stolen backup is not a shortcut around the controls on the live database.
Security headers and CSP
Enforced on every response: a strict Content-Security-Policy, Strict-Transport-Security, X-Content-Type-Options, and a locked-down frame-ancestors to prevent clickjacking of merchant admin surfaces.
Rate limiting
Applied on all public, unauthenticated endpoints (webhooks, storefront checkout, auth) to shed abusive traffic at the edge before it reaches application logic.
Backups, retention and restore testing
A backup that has never been restored is a hope, not a control. Backups run hourly, retained on a rolling window sized for point-in-time recovery, with restore drills rehearsed into an isolated environment and verified against expected row counts before a drill is called successful.
| Retention by data class | Backup frequency | Restore drill cadence | Deletion on request |
|---|---|---|---|
| Transactional (orders, payments) | Hourly | Scheduled, documented | Retained per legal tax record requirement, then purged |
| Operational (products, inventory, staff) | Hourly | Scheduled, documented | Deleted on confirmed request |
| Observability (metrics/logs/traces) | N/A — bounded retention | N/A | Aged out automatically |
| Customer PII (profile, address) | Hourly | Scheduled, documented | Deleted on confirmed request, subject to legal hold if applicable |
Observability and alerting contract
Our observability stack is entirely self-hosted — Prometheus, Grafana, Loki/Promtail, Sentry, Alertmanager — for the same reason our data layer is self-hosted: no vendor can throttle our visibility into our own platform, and retention is a decision we make, not a plan tier we buy.
| Observability signals | What it captures | Path | Retention |
|---|---|---|---|
| Metrics | Request rates, error rates, latency, queue depth | App → Prometheus scrape (30s) | 30 days / 20GB |
| Logs | Structured JSON, PII-scrubbed, carrying trace_id/span_id | App stdout → Promtail → Loki | 30 days |
| Errors & traces | Exceptions and distributed traces | App → self-hosted Sentry | Per configured Sentry quota |
| Alerts | Burn-rate and threshold rules | Prometheus → Alertmanager → PagerDuty / Slack | — |
- severity=page is reserved for user-visible loss or imminent data risk. Everything else opens a ticket, not a page.
- Every alerting rule carries a runbook annotation — the person paged at 3am gets a link to the exact steps, not a bare metric name.
- Pages are driven by burn-rate, not raw error counts, so a brief self-healing blip does not wake anyone.
- The stack watches itself: an exporter down or ingestion stalled is its own alert — the monitoring system failing silently is treated as seriously as the product failing.
Incident response
Incident response runbook
On-call classifies severity within minutes of acknowledgement, not after full root-cause is known. The fastest safe action that stops ongoing harm — revoke a key, disable a route, roll back a deploy — happens before root cause is fully understood. Affected merchants are notified directly for anything SEV-1 or SEV-2 with merchant impact: what we know, what we don't yet know, and what we're doing next.
- Affected merchants are notified directly — email plus in-dashboard notice — not left to discover impact themselves.
- Timelines in the severity table are commitments, not aspirations; if we miss one, the post-mortem says so.
- We disclose what we know when we know it, and follow up as the picture completes.
- A material incident gets a public-facing write-up; we do not go quiet after the immediate fire is out.
- 1Detect
- 2Triage & assign severity
- 3Contain
- 4Notify affected merchants
- 5Eradicate & fix
- 6Recover
- 7Public/customer write-up
- 8Post-mortem
| Incident severity classification | Definition | Example | Paged? | First merchant update |
|---|---|---|---|---|
| SEV-1 | Data breach, cross-tenant data exposure, or platform-wide outage | RLS bypass discovered; checkout down platform-wide | Immediate page, on-call + security lead | Within 1 hour of confirmation |
| SEV-2 | Significant degraded service or a contained security issue affecting a subset of merchants | Elevated checkout error rate; one integration's key leaked | Immediate page, on-call | Within 4 hours |
| SEV-3 | Limited-impact bug or a vulnerability with no evidence of exploitation | A dependency CVE with no known exploit path in our usage | Ticket, next business day | Included in routine disclosure if applicable |
| SEV-4 | Cosmetic or non-security operational issue | A dashboard chart mislabels a unit | Ticket | Not applicable |
Vulnerability disclosure policy
We welcome good-faith security research and would rather hear from you first.
- 1Email security@framique.com (placeholder pending final domain configuration) with a description, reproduction steps and impact assessment.
- 2Do not test against live merchant stores you do not own or operate; use a test account or ask us to provision one.
- 3Give us a reasonable window to investigate and remediate before any public disclosure — we aim to acknowledge within 2 business days and provide a remediation timeline within 10 business days for confirmed issues.
- 4We will not pursue legal action against good-faith research conducted under this policy.
- In scope
- The production application and API surfaces at *.framique.com and merchant subdomains.
- Out of scope
- Third-party subprocessor infrastructure (report to them directly), denial-of-service testing, and social engineering of staff or merchants.
Out of scope for now: a paid bug bounty program is not yet running (see the roadmap band); we still want the report, and we will credit researchers publicly on request.
Dependency and supply-chain scanning
- Automated dependency scanning runs against every change, flagging known-vulnerable packages before merge.blocks release
- A confirmed high or critical severity finding with a known exploit path blocks release until patched or explicitly risk-accepted by a named engineer, in writing, with a remediation deadline.blocks release
- Lockfiles are committed and CI verifies the resolved dependency tree matches the lockfile.verified in CI
- Client bundles are scanned before release for accidental inclusion of server-only code or secrets.blocks release
- Base images and infrastructure containers are pinned to specific versions rather than tracking latest.pinned, not latest
- Internal code review requires at least one other engineer's approval before merge to main.reviewed change
Self-hosting and data residency options
Framique is built self-hosted-first: Postgres, Redis, auth, storage and the entire observability stack run on infrastructure we control, and the application only ever talks to them through swappable, provider-agnostic connection strings — nothing in the application code names a specific hosting provider.
Standard hosting
Dedicated / regional hosting
Fully self-hosted (enterprise)
Subprocessor transparency table
We minimise third parties in the data path by design — most of the stack is self-hosted precisely to avoid an ever-growing subprocessor list. Full, current subprocessor names and jurisdictions are listed in our data processing agreement, kept current as agreements change; this page describes the categories and boundaries.
| Subprocessor categories | Purpose | Data involved | Location commitment |
|---|---|---|---|
| Payment processing (MFS/bank/BNPL partners) | Processing charges, refunds and payouts initiated by the merchant | Transaction reference, amount, masked payment details | Per processor's own regulatory jurisdiction; raw credentials never transit our servers |
| Courier partners | Fulfilling shipments the merchant creates | Recipient name, address, phone, order reference | Bangladesh-based courier operations |
| Infrastructure hosting | Running self-hosted Postgres, Redis, and the application | All merchant data, self-hosted | Per hosting/residency option selected |
| Email/SMS delivery (transactional) | Order confirmations, OTPs, account notifications | Recipient contact detail, message content | Disclosed in the data processing agreement on request |
Full, current subprocessor names live in our legal documents, the contractual source of truth.
Your side of the security checklist
Security is shared: we harden the platform, and these ten habits close the gaps only the merchant controls.
- Turn on two-factor authentication for every staff account with owner or manager access.
- Use the least-privileged role for each staff member.
- Rotate API keys on a schedule (90 days) and immediately after any staff departure who had access to one.
- Scope every API key narrowly — a courier integration needs order and shipping scopes, not payout access.
- Review the audit log periodically, especially after a staff change, for actions you don't recognise.
- Never share login credentials over chat, email or phone — including with anyone claiming to be Framique support.
- Set a refund cap appropriate to staff trust level rather than leaving it unlimited by default.
- Verify webhook endpoints you configure are HTTPS and validate the signature we send.
- Keep a designated security contact on file with us so incident notifications reach a real person.
- Test your own restore/export process at least once — know how to get your data out before you ever need to.
Compliance roadmap — clearly marked
This band lists work in progress. Nothing here is a current certification or audit status. Items move out of this band only once genuinely completed, and this page is updated at that point — not before.
| Initiative | Status | What it means when complete |
|---|---|---|
| Formal penetration test by an independent third party | Roadmap | External validation of the isolation model and API surface, with findings remediated and summarised publicly |
| Bug bounty program | Roadmap | A standing paid incentive for external researchers, replacing the current goodwill disclosure process |
| SOC 2 Type II readiness review | Roadmap | Not a claim of certification today; a scoped effort to align controls with SOC 2 Type II criteria ahead of a future audit |
| PCI DSS scope reduction review | Roadmap | Formal confirmation of the tokenisation boundary, ahead of any assessment |
| ISO 27001 gap assessment | Roadmap | Structured comparison of current practices against the standard, as a precursor to a certification decision |
Frequently asked
Do you hold SOC 2 or ISO 27001 certification today?
Where does merchant data physically live?
Can one merchant ever see another merchant's data through a bug in your app?
Do you ever see our customers' full card numbers?
What happens if there's a data breach?
Can we run Framique entirely on our own infrastructure?
How do we report a security vulnerability we found?
What do you log, and could our secrets end up in a log file?
How often are backups tested, not just taken?
Who can access our store's data on your side, and is it logged?
Send us your security questionnaire.আপনার সিকিউরিটি প্রশ্নপত্র আমাদের পাঠান।
Most vendor security reviews map directly onto the bands above. Send us yours and we'll respond with citations back to this page and our data processing agreement, not a generic template.