| Question | Answered by |
|---|---|
| May this database be here at all? | the residency check in @Jurisdiction |
| May this row be sent there? | CrossBorderLog.checkTransferAllowed() |
| Where has it been sent, and on what basis? | CrossBorderLog.logTransfer() and the transfer table |
residency rule:interface ResidencyRule {
localize: 'none' | 'copy' | 'primary';
regions: string[]; // 'eu', 'eea', 'us', 'cn', … — the region vocabulary
transferRequires: 'nothing' | 'adequacy' | 'safeguards' | 'consent' | 'approval';
}import { Jurisdiction, Table, Model, applyCompliance } from 'ts-prorm-orm';
@Jurisdiction('CN')
@Table({ tableName: 'customers' })
class Customer extends Model { /* … */ }
// → refused: residency-unknown — China requires the primary copy to be in cn, and no region is declared.
@Jurisdiction('CN', { region: 'us' }) // the trailing object carries what no column can
@Table({ tableName: 'customers' })
class CustomerInUs extends Model { /* … */ }
// → refused: residency-violated — the database is in "us"; China requires the primary copy to be in cn.
@Jurisdiction('CN', { region: 'cn' })
@Table({ tableName: 'customers' })
class CustomerInCn extends Model { /* … */ }
// → loads.
applyCompliance(Customer, { jurisdictions: ['EU'], region: 'us', fields: { /* … */ } });
// → loads, with an informational finding: transfer-outside — the database is
// outside eu/eea; transfers need adequacy.'eu' or 'eea' for the EU, not a provider's 'eu-west-1'; map yourcompliance.residency.* policies hold it there@Jurisdiction('EU', 'RU') must be in Russia.@DatabaseSecurity({ region, allowedCountries }) records the same facts asCrossBorderLog holds a table of regions with an adequacy flag and theimport { CrossBorderLog } from 'ts-prorm-orm';
await CrossBorderLog.checkTransferAllowed('EU', 'US', 'PII');
// { allowed: false, requiresReview: true,
// reason: 'Transfer to US requires additional safeguards (SCCs, BCRs, or adequacy decision)', … }
await CrossBorderLog.checkTransferAllowed('EU', 'UK', 'PII');
// { allowed: true, … }
await CrossBorderLog.checkTransferAllowed('EU', 'US', 'GENERAL');
// { allowed: true, reason: 'Transfer to US - target does not have adequate protection determination', … }requires_scc), Chinarequires_assessment), India, Brazil, Thailand, Singapore, Australia andPII and PHI are refused to a non-adequaterequiresReview: true. A code'MX', say) is refused with Unknown source
region or Unknown target region; only the literal 'UNKNOWN' is allowedCrossBorderLog.configureRegions({
EU: { adequateProtection: true, regulations: ['GDPR'] },
US: { adequateProtection: true, regulations: ['CCPA'], requirements: { mechanism: 'EU-US DPF' } },
CN: { adequateProtection: false, regulations: ['PIPL'], restrictions: ['requires_assessment'] },
});
CrossBorderLog.listRegions();
CrossBorderLog.resetRegions(); // back to the built-in tableCrossBorderLog.enable(prorm, Customer, {
getUserId: () => currentUser()?.id,
getIpAddress: () => currentRequest()?.ip,
tableName: 'cross_border_transfers', // default
});
await CrossBorderLog.logTransfer('EU', 'US', 'PII', rows, {
initiatedBy: currentUser()?.id,
purpose: 'support ticket escalation',
legalBasis: 'SCCs 2021/914, module 2',
status: 'completed', // 'pending' | 'completed' | 'blocked' | 'reviewed'
});
await CrossBorderLog.getTransferHistory(Customer, customerId);
await CrossBorderLog.getAllTransfers(Customer, { sourceRegion: 'EU', status: 'blocked' });logTransfer() is called by you. applyHooks() / theautoLog option stash the context on the model and do not logfindAll() is about to leaveimport { CrossBorderLog } from 'ts-prorm-orm';
declare const Customer: any, Ticket: any;
declare const vendor: { send(rows: unknown[]): Promise<void> };
export async function escalateToVendor(customerId: string, agentId: string) {
const rows = await Ticket.findAll({ where: { customerId } });
const verdict = await CrossBorderLog.checkTransferAllowed('EU', 'US', 'PII');
if (!verdict.allowed) {
await CrossBorderLog.logTransfer('EU', 'US', 'PII', rows, {
recordId: customerId, initiatedBy: agentId,
purpose: 'support ticket escalation', status: 'blocked',
metadata: { reason: verdict.reason },
});
throw new Error(`transfer refused: ${verdict.reason}`);
}
await vendor.send(rows);
await CrossBorderLog.logTransfer('EU', 'US', 'PII', rows, {
recordId: customerId, initiatedBy: agentId,
purpose: 'support ticket escalation',
legalBasis: 'SCCs 2021/914, module 2',
status: verdict.requiresReview ? 'pending' : 'completed',
});
}PII. Once configureRegions() records your transferlegalBasis.@Jurisdiction('EU', { region: 'us' }) loads, and the sheet carries thetransfer-outside finding so nobody forgets the data is exported by itsCrossBorderLog.configureRegions() with your actual mechanism (DPF, SCCs).checkTransferAllowed('EU', 'US', category) in the paths that send datalogTransfer() on the way out.@Security({ frameworks: [SecurityFramework.Schrems_II] })