Technical note · Production schema

The DPDP Sec. 6/Sec. 7 split is a database column, not a policy statement.

Some vendors position themselves as “DPDP-native” while the underlying schema is GDPR from 2018 with a language toggle. This page shows dcomply's real production migrations. The India-specific requirements are enforced at the schema level, so they cannot drift out of alignment with the marketing copy.

processing_activities schema
// Sec. 5 lawful basis is a database column, // not a spreadsheet tag $table->string('legal_basis'); // consent | legitimate_interest // legal_obligation | vital_interest // public_task | contract // Sec. 16 cross-border check baked in $table->boolean('cross_border'); $table->foreignId('cross_border_transfer_id'); // Sec. 10 DPIA gate as a schema // constraint, not a workflow reminder $table->boolean('dpia_required'); $table->foreignId('dpia_id');
Statutory factor 1

Sec. 6 consent vs Sec. 7 legitimate use is a column, not a policy statement

The DPDP Act splits every processing activity into either Sec. 6 consent-based or Sec. 7 legitimate-use grounds. A GDPR-adapted platform typically stores “lawful basis” as a free-text field on the RoPA export. dcomply enforces it at the schema level as an enumerated column.

From database/migrations/2026_04_27_500001_create_processing_activities_table.php:

Schema::create('processing_activities', function (Blueprint $table) { $table->id(); $table->foreignId('tenant_id')->constrained('tenants'); $table->string('activity_id')->unique(); $table->string('name'); $table->text('purpose'); // The Sec. 6/Sec. 7 split. Enumerated at the application layer, // enforced by validation, and exported as-is to the DPBI RoPA report. $table->string('legal_basis'); // consent → Sec. 6 (Consent) // legitimate_interest → Sec. 7(1)(b) certain legitimate uses // legal_obligation → Sec. 7(1)(g) compliance with law // vital_interest → Sec. 7(1)(h) medical emergency // public_task → Sec. 7(1)(a) State functions // contract → Sec. 7(1)(f) performance of contract // Data categories per Sec. 5 notice obligation $table->json('data_categories'); // personal | sensitive | financial | health | biometric | children $table->json('data_subjects'); // employees | customers | minors | vendors $table->integer('retention_days')->nullable(); // Sec. 8(7) requires per-purpose retention. Storing it as an integer // means the retention runner can enforce it programmatically. });

Why this matters

When the DPBI asks for a Record of Processing Activities under Sec. 8(4), the query is one line: SELECT name, legal_basis, data_categories, retention_days FROM processing_activities WHERE tenant_id = ?. The report cannot lie about the lawful basis because every consent-based activity is joined to the actual consent records via consent_purpose_logs.action = 'given'. If the RoPA claims consent but there is no matching consent event, the join surfaces the gap.

Statutory factor 2

Sec. 9 children data as a first-class type

GDPR treats children as “special category with parental consent for under-16” via configuration. DPDP Sec. 9 mandates verifiable parental consent for every processing of an under-18 principal's data and prohibits behavioural tracking or targeted advertising. dcomply models this on the consent record directly.

// From consent_records table, migrations 2026_04_20_100005_add_minor_consent_fields.php $table->boolean('is_minor')->default(false); $table->string('parent_name')->nullable(); $table->string('parent_email')->nullable(); // encrypted at column level $table->string('parent_phone')->nullable(); // encrypted at column level $table->string('parent_relationship')->nullable(); $table->string('parental_consent_status'); // pending | verified | rejected $table->timestamp('parental_consent_at')->nullable(); $table->string('parental_verification_token')->nullable();

From the ConsentRecord model:

// PII fields encrypted at rest using APP_KEY via AES-256-CBC protected $casts = [ 'is_minor' => 'boolean', 'parental_consent_at' => 'datetime', 'parent_email' => 'encrypted', // column-level AES-256 'parent_phone' => 'encrypted', // column-level AES-256 ];

A GDPR schema cannot cleanly do this

GDPR-derived schemas often carry “parental_consent_needed: bool” and the actual guardian details as free-text. Retrofitting Sec. 9(2) verifiable-parent workflow onto that layout requires application-code workarounds. Because dcomply models the guardian as a first-class object with encrypted contact fields and a verification token, the audit trail per Sec. 9(1) is a direct SELECT.

Statutory factor 3

Sec. 16 cross-border transfer as a per-activity boolean

The DPDP Act permits the Central Government to notify countries to which personal data may not be transferred. Every processing activity must therefore know whether it crosses a border, and every cross-border activity must know which mechanism authorises it. dcomply enforces both as schema constraints.

// From processing_activities schema: $table->boolean('cross_border')->default(false); $table->foreignId('cross_border_transfer_id') ->nullable() ->constrained('cross_border_transfers') ->nullOnDelete(); // From cross_border_transfers table, this points at: // - destination country // - authorising mechanism (Central Government notification, contractual clauses) // - safeguards documented (encryption, DPA, sub-processor chain)
Schema-level check. When cross_border = true and cross_border_transfer_id IS NULL, the model's isValidForProcessing() guard returns false. A processing activity that crosses a border without a transfer record cannot be marked active. This is enforced by the model's activation validator, not by workflow reminders.
Statutory factor 4

Sec. 10 DPIA as a schema constraint, not a workflow reminder

When a Significant Data Fiduciary undertakes high-risk processing, Sec. 10(2)(c) mandates a periodic DPIA. dcomply enforces this by binding every processing activity to a nullable DPIA record and blocking activation when the flag is set but the record is missing.

// Schema-level DPIA binding: $table->boolean('dpia_required')->default(false); $table->foreignId('dpia_id') ->nullable() ->constrained('dpias') ->nullOnDelete(); // Model guard (simplified): public function isMissingDpia(): bool { return $this->dpia_required && !$this->dpia_id; } public function isValidForProcessing(): bool { return !$this->isMissingDpia() && !$this->isMissingCrossBorderRecord(); }

The audit story

A DPBI inquiry into an SDF asks: “show me the DPIA for every high-risk activity.” The dcomply answer is a JOIN between processing_activities and dpias filtered by dpia_required = true. If any row is missing, it never reached status = 'active', so the SDF cannot be accused of live processing without a DPIA. See the SDF Classification Checker.

Statutory factor 5

Rule 3 Eighth Schedule languages as translation records

Draft Rule 3(1) mandates that every notice be made available in English and each of the 22 languages listed in the Eighth Schedule of the Constitution. A GDPR schema treats language as a rendering-time option. dcomply stores every translation as a row and hashes each translation body so the exact notice served can be replayed.

// From migrations/2026_04_20_200005_create_multilingual_notices_table.php Schema::create('multilingual_notices', function (Blueprint $table) { $table->id(); $table->foreignId('tenant_id')->constrained('tenants'); $table->string('notice_id')->unique(); $table->string('title'); $table->text('base_content'); $table->string('base_language')->default('en'); // Per-language body content, keyed by ISO 639-1 code. // {"hi": "...", "ta": "...", "bn": "...", "mr": "..."} $table->json('translations'); // Which of the 22 languages the tenant has opted to serve. $table->json('selected_languages'); $table->string('notice_type')->default('privacy_notice'); $table->string('version')->default('1.0'); $table->string('status')->default('draft'); $table->timestamp('published_at')->nullable(); });

Model helpers (MultilingualNotice.php) surface the Eighth Schedule check directly:

public function hasTranslationFor(string $lang): bool { return !empty($this->translations[$lang] ?? null); } public function hasAllTranslations(): bool { foreach ($this->selected_languages as $lang) { if (!$this->hasTranslationFor($lang)) return false; } return true; } public function hasRequiredRegionalLanguage(string $state): bool { $mandatory = config('india.state_language_map')[$state] ?? null; return $mandatory === null || $this->hasTranslationFor($mandatory); }
Statutory factor 6

Sec. 8(9) accountability enforced by SHA-256 chain, not a log statement

Sec. 8(9) requires the Data Fiduciary to demonstrate compliance. A conventional audit log is a claim, not evidence. dcomply's EvidenceChain is a per-tenant hash-chained ledger that a regulator can verify offline against the DPBI export bundle.

// From App\Services\Discovery\EvidenceChain.php $prevHash = $previous?->current_hash; $canonical = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR); $currentHash = hash('sha256', ($prevHash ?? '') . '|' . $canonical . '|' . $occurredAt->format('Y-m-d H:i:s')); // Append-only. No update or delete path exists in application code. return SourceEvidenceEntry::create([ 'tenant_id' => $tenantId, 'event_type' => $eventType, 'payload' => $payload, 'previous_hash' => $prevHash, 'current_hash' => $currentHash, 'occurred_at' => $occurredAt, ]);

See the full EvidenceChain architecture or run the Consent Integrity Demo to break a chain in your browser.

GDPR-adapted vs India-native

Where a GDPR schema struggles under DPDP

A GDPR-first product retrofits India as a region. It works. It just leaks in six specific places.

DPDP requirement GDPR-adapted platform India-native dcomply
Sec. 6/Sec. 7 Lawful basis split Article 6 lawful bases (6 grounds) mapped onto DPDP via config. RoPA report contains the GDPR ground. Enumerated legal_basis column on every processing activity. Six DPDP grounds. Report exports the DPDP-native value.
Sec. 9 Children data GDPR Article 8 covers under-16 with parental consent via configuration. India's under-18 rule requires re-configuration. Every consent record carries is_minor, encrypted parent_email, verification token, and parental_consent_status enum. Under-18 is the default.
Sec. 16 Cross-border transfer SCCs, adequacy decisions, BCRs are the GDPR default. India requires Central Government notification, checked case by case. Boolean cross_border flag on activity + FK to cross_border_transfers. Activity cannot activate without a valid transfer record.
Sec. 10(2)(c) Periodic DPIA GDPR DPIA is triggered by risk assessment. Periodic requirement is configuration. dpia_required boolean on activity + FK to dpias. isValidForProcessing() guard returns false when required and missing.
Sec. 5(3) Regional language One language per notice. English fallback. i18n at rendering time. JSON translations map per notice, keyed by 22 Eighth Schedule language codes. hasAllTranslations() gate before publish.
Sec. 8(6) Breach 72h to DPBI GDPR 72h to supervisory authority. Notification template targets a European DPA structure. notification_deadline as a datetime column. isUrgent() and isOverdue() flip based on India time. Live countdown widget on the breach show page.
Sec. 8(9) Accountability evidence Audit log with timestamps. Not hash-chained. A DBA can rewrite historic rows. source_evidence_entries with SHA-256 previous_hash + current_hash. Any historic rewrite is caught by the verify() walk.

Schema is opinion made durable.

If your DPDP platform's marketing copy talks about India-native design, ask them to show you the migrations.