ADR-004: TCA integration
Table of contents
Status
Accepted
Date
2026-01-03
Context
TYPO3 extensions commonly store sensitive data (API keys, credentials, tokens) in database fields configured via TCA. The nr-vault extension needs to provide a seamless way to store these values securely without requiring extensions to rewrite their data handling.
The integration must:
- Work with existing TCA field configurations
- Handle record operations (create, update, delete, copy)
- Support both regular TCA fields and FlexForm fields
- Maintain the TYPO3 backend user experience
Problem statement
How should nr-vault integrate with TYPO3's TCA system to transparently encrypt sensitive fields while maintaining standard TYPO3 workflows?
Decision drivers
- Transparency: Extensions should need minimal code changes
- Compatibility: Must work with standard TYPO3 record operations
- User experience: Backend users should see familiar interfaces
- Flexibility: Support various field types and configurations
- Auditability: All operations must be trackable
Considered options
Option 1: Custom field type
Create a completely new TCA field type.
Pros:
- Full control over behavior
Cons:
- Requires TCA rewrite for existing extensions
- Different behavior from standard fields
Option 2: FormEngine override
Override the default input field rendering globally.
Pros:
- No TCA changes needed
Cons:
- Affects all input fields
- Difficult to target specific fields
- Potential conflicts
Option 3: Custom renderType with DataHandler hooks
Provide a renderType for FormEngine and intercept saves via hooks.
Pros:
- Opt-in per field (add
renderType: 'vaultSecret') - Uses standard TYPO3 hook system
- Familiar pattern for TYPO3 developers
Cons:
- Requires TCA modification (but minimal)
- Two components to maintain (element + hook)
Decision
We chose custom renderType with DataHandler hooks because:
- Explicit opt-in: Only fields marked with
renderType: 'vaultSecret'are encrypted - Standard patterns: Uses FormEngine elements and DataHandler hooks
- Minimal changes: One line added to existing TCA configurations
- Full lifecycle: Hooks handle create, update, delete, and copy operations
Implementation
FormEngine element
final class VaultSecretElement extends AbstractFormElement
{
public function render(): array
{
// Render password field with:
// - Masked display (dots)
// - Reveal button (permission-based)
// - Copy button (permission-based)
// - Hidden field for vault identifier
}
}
Registration in ext_:
$GLOBALS['TYPO3_CONF_VARS']['SYS']['formEngine']['nodeRegistry'][1735400000] = [
'nodeName' => 'vaultSecret',
'priority' => 40,
'class' => VaultSecretElement::class,
];
DataHandler hook
final class DataHandlerHook
{
// Before save: Extract secret, generate UUID, queue for storage
public function processDatamap_preProcessFieldArray(...): void
{
foreach ($this->getVaultFields($table) as $field) {
if ($this->hasSecretValue($fieldArray, $field)) {
$uuid = $this->generateUuid();
$this->pendingSecrets[$table][$id][$field] = [
'uuid' => $uuid,
'value' => $fieldArray[$field]['value'],
];
$fieldArray[$field] = $uuid; // Store UUID in database
}
}
}
// After save: Store secrets with correct UID
public function processDatamap_afterDatabaseOperations(...): void
{
foreach ($this->pendingSecrets[$table][$id] as $field => $data) {
$this->vaultService->store($data['uuid'], $data['value'], [
'metadata' => [
'table' => $table,
'field' => $field,
'uid' => $recordUid,
'source' => 'tca_field',
],
]);
}
}
// Before delete: Remove associated secrets
public function processCmdmap_preProcess(...): void;
// After copy: Create new secrets for copied record
public function processCmdmap_postProcess(...): void;
}
FlexForm hook
Separate hook for FlexForm fields due to different data structure:
final class FlexFormVaultHook
{
public function processDatamap_preProcessFieldArray(...): void
{
// Recursively scan FlexForm XML for vaultSecret fields
// Same UUID-based approach as TCA fields
// Store metadata: flexField, sheet, fieldPath
}
}
TCA configuration
Extensions add vault support with one line:
'api_key' => [
'label' => 'API Key',
'config' => [
'type' => 'input',
'renderType' => 'vaultSecret', // This one line
'size' => 30,
],
],
Helper for common patterns:
use Netresearch\NrVault\TCA\VaultFieldHelper;
'api_key' => VaultFieldHelper::getSecureFieldConfig('API Key'),
Data flow
Form Display:
1. VaultSecretElement renders password field
2. If UUID exists, shows masked value with reveal option
3. JavaScript handles reveal/copy interactions
Form Submit:
1. DataHandlerHook.preProcess extracts secret value
2. Generates UUID v7 identifier (see ADR-001)
3. Sets field value to UUID (for database)
4. DataHandlerHook.afterDatabaseOperations stores secret in vault
Record Delete:
1. DataHandlerHook.processCmdmap_preProcess finds vault fields
2. Retrieves UUIDs from record
3. Deletes corresponding vault secrets
Record Copy:
1. DataHandlerHook.processCmdmap_postProcess detects copy
2. Retrieves source secrets by UUID
3. Creates new secrets with new UUIDs for copied record
Runtime resolution
use Netresearch\NrVault\Utility\VaultFieldResolver;
// Resolve specific fields
$resolved = VaultFieldResolver::resolveFields($record, ['api_key']);
// Auto-detect vault fields from TCA
$resolved = VaultFieldResolver::resolveRecord('tx_myext_settings', $record);
Consequences
Positive
- Minimal migration: Add
renderTypeto existing fields - Familiar patterns: Standard FormEngine and DataHandler usage
- Full lifecycle: Handles all record operations automatically
- Audit trail: All operations logged with context metadata
- UUID portability: Secrets not tied to table structure
Negative
- Two hooks required: Separate handling for TCA and FlexForm
- Runtime resolution: Application code must resolve UUIDs to values
- Learning curve: Developers must understand vault resolution
Risks
- Hook execution order conflicts with other extensions
- FlexForm structure changes could break field detection
Mitigation
- Use high priority for hooks
- Comprehensive test coverage for FlexForm parsing
- Clear documentation for resolution patterns