ADR-004: TCA integration 

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:

  1. Explicit opt-in: Only fields marked with renderType: 'vaultSecret' are encrypted
  2. Standard patterns: Uses FormEngine elements and DataHandler hooks
  3. Minimal changes: One line added to existing TCA configurations
  4. Full lifecycle: Hooks handle create, update, delete, and copy operations

Implementation 

FormEngine element 

Classes/Form/Element/VaultSecretElement.php
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
    }
}
Copied!

Registration in ext_localconf.php:

ext_localconf.php
$GLOBALS['TYPO3_CONF_VARS']['SYS']['formEngine']['nodeRegistry'][1735400000] = [
    'nodeName' => 'vaultSecret',
    'priority' => 40,
    'class' => VaultSecretElement::class,
];
Copied!

DataHandler hook 

Classes/Hook/DataHandlerHook.php
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;
}
Copied!

FlexForm hook 

Separate hook for FlexForm fields due to different data structure:

Classes/Hook/FlexFormVaultHook.php
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
    }
}
Copied!

TCA configuration 

Extensions add vault support with one line:

Configuration/TCA/tx_myext_settings.php
'api_key' => [
    'label' => 'API Key',
    'config' => [
        'type' => 'input',
        'renderType' => 'vaultSecret',  // This one line
        'size' => 30,
    ],
],
Copied!

Helper for common patterns:

Using VaultFieldHelper
use Netresearch\NrVault\TCA\VaultFieldHelper;

'api_key' => VaultFieldHelper::getSecureFieldConfig('API Key'),
Copied!

Data flow 

TCA vault field 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
Copied!

Runtime resolution 

Resolving secrets in application code
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);
Copied!

Consequences 

Positive 

  • Minimal migration: Add renderType to 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

References