"We'll handle compliance later" is the most expensive sentence in software engineering. The average data breach costs $4.88 million. Equifax's cost over $1.4 billion. These aren't outliers - they're the predictable result of bolting compliance on after the fact.
Compliance isn't a feature. It's an architectural property. You either design for it from the start or you rebuild later. This is based on our experience building InvestYadnya (SEBI-regulated financial advisory) and AskMeAnything.life (payment processing and user data handling).
The audit trail is not optional
Every regulated product needs to answer: "What happened, when, and who did it?" If a regulator, auditor, or customer asks about a transaction from six months ago, you need to produce a complete history of every state change.
Simple History: every change, tracked automatically
We use django-simple-history on every model that touches money or compliance. Adding history = HistoricalRecords() to a model gives you a complete changelog: who changed what field, when, and what the previous value was. This happens at the ORM level - it's impossible to update a record without the change being logged.
For financial records, we go further: the Django admin is configured as view-only. has_add_permission and has_delete_permission return False. Support staff can look up any order and see its full history, but they can't modify or delete anything. This isn't paranoia - it's mandated by SEBI's Investment Advisers Regulations, 2013 (Regulation 19), which requires investment advisers to maintain records of all activities for a minimum of five years, with annual compliance audits.
Event logs for business-level audit
Simple History tracks field-level changes. But regulators also want to know what business events occurred. For this, we use a JSONField on the model itself - an append-only event log: order_created, payment_captured, license_created, subscription_renewed - each with structured data (payment IDs, amounts, timestamps). Two layers: field-level tracking for debugging, business-event tracking for reporting.
KYC: identity verification as a first-class flow
For SEBI-regulated products in India, KYC isn't a checkbox - it's a prerequisite. InvestYadnya integrates with Digio for KYC verification through India's KRA (KYC Registration Agency): PAN status check against the KRA registry, Aadhaar-based e-Sign for investment agreements, and full storage of every response for SEBI audit compliance.
Our ESignRequest model captures the entire compliance chain: user, KRA response, template, signer details, sign type, status, and failure reason. Every field exists because a regulator might ask for it. The model uses on_delete=RESTRICT on the user foreign key - you can't delete a user who has compliance records, even if a bug tries.
RBAC: permissions tied to what you've paid for
Role-based access control in regulated products isn't just "admin vs user." In a financial platform, access to specific features depends on what the user has purchased and whether their subscription is current.
Our permission model:
- A base
HasPlanAccesspermission class that queries active licenses at request time - Specific permissions like
HasFinancialPlanAccessthat specify which plan is required - An
.active()QuerySet method that filters by status AND checkscurrent_period_endagainst the current time
This means expired subscriptions are locked out automatically - no cron job, no background task, no race condition. Every API request checks the current state of the license at that exact moment.
It's tempting to cache a user's access level to avoid a database query per request. Don't. When a subscription expires at 11:59 PM, the next request at 12:00 AM must be denied. Caching introduces a window where expired users still have access. In regulated products, that window is a compliance violation.
Data integrity at the database level
Application-level validation is not enough. Bugs happen. Race conditions happen. The database is your last line of defense.
Constraints we use on every financial model
RESTRICTon foreign keys - Users with orders can't be deleted. Orders with licenses can't be deleted. The dependency chain is enforced by the database, not by application code.- Unique constraints -
(user, shopify_variant_id)on licenses prevents duplicate subscriptions.razorpay_order_idis unique to prevent duplicate order creation.event_idon webhook events prevents duplicate processing. - Check constraints -
current_period_end >= current_period_startensures license periods are logically valid. The database rejects any write that violates this. DecimalFieldfor currency - Alwaysmax_digits=12, decimal_places=2. Never float. This is enforced at the schema level.
Each of these prevents a specific bug we've seen in production elsewhere: duplicate webhooks creating duplicate licenses, negative subscription periods, floating-point rounding on invoices. Not theoretical - structurally impossible.
Structured logging for everything
When a regulator asks "what happened with this payment?", plaintext logs are useless. We use structlog for structured JSON logging - every entry includes event name, identifiers (order_id, payment_id, user_id), timestamp, and severity. In GCP Cloud Logging, you filter by razorpay_payment_id = "pay_xyz" and see every related log entry in order. Thirty-minute investigation becomes a 30-second query.
GDPR and data residency
If you serve European users, GDPR affects your architecture from day one. The fines are real - Meta: EUR 1.2 billion, Amazon: EUR 746 million, TikTok: EUR 530 million - and regulators are working down the list.
The engineering requirements: data residency (region-aware infrastructure), right to deletion (delete personal data while retaining anonymized financial records), consent tracking (when and for what purpose), and data export in portable formats. The RESTRICT cascade on foreign keys helps - when someone requests deletion, the database tells you exactly which dependent records exist instead of silently cascade-deleting audit records.
Accessibility as compliance
Accessibility is a legal requirement in more jurisdictions than most teams realize. The European Accessibility Act became enforceable across 27 EU member states in June 2025. The WebAIM Million report found 95.9% of the top million websites have detectable WCAG failures - an average of 56 errors per page. These are basic engineering failures: low contrast, missing alt text, missing form labels.
The patterns that prevent this: semantic HTML (<button> not <div onclick>), keyboard navigation for every interactive element, axe-core in CI for automated testing, and WCAG 2.1 AA color contrast built into the design system from day one.
The compliance-first checklist
Before writing your first line of code, ask:
- Which regulations apply? GDPR, SEBI, PCI-DSS, ADA, HIPAA - each has specific engineering implications.
- What needs an audit trail? Any state change involving money, personal data, or access control.
- What can't be deleted? Financial records, compliance documents, audit logs. Design your data model with retention requirements in mind.
- Who can access what? Define your permission model before building features. Retrofitting RBAC is painful.
- Where does the data live? Data residency requirements affect your infrastructure choices on day one.
None of these questions are hard to answer early. They're extremely hard to answer after you've built a product that assumed none of them mattered.
Compliance-first isn't slower. It's the only approach that doesn't require a rewrite when the regulator comes calling.