---
title: "ADR-004: TCA integration"
manual: "nr-vault"
version: "1.0"
permalink: "https://docs.typo3.org/permalink/netresearch/nr-vault:adr-004-tca-integration@1.0"
source: "Developer/Adr/ADR-004-TcaIntegration.rst"
rendered: "2026-09-18T07:37:50+00:00"
---

# ADR-004: TCA integration {#adr-004-tca-integration-1}

**Table of contents**

-   [Status](https://docs.typo3.org/permalink/netresearch/nr-vault:status@1.0)
-   [Date](https://docs.typo3.org/permalink/netresearch/nr-vault:date@1.0)
-   [Context](https://docs.typo3.org/permalink/netresearch/nr-vault:context@1.0)
-   [Problem statement](https://docs.typo3.org/permalink/netresearch/nr-vault:problem-statement@1.0)
-   [Decision drivers](https://docs.typo3.org/permalink/netresearch/nr-vault:decision-drivers@1.0)
-   [Considered options](https://docs.typo3.org/permalink/netresearch/nr-vault:considered-options@1.0)
-   [Decision](https://docs.typo3.org/permalink/netresearch/nr-vault:decision@1.0)
-   [Implementation](https://docs.typo3.org/permalink/netresearch/nr-vault:implementation@1.0)
-   [Consequences](https://docs.typo3.org/permalink/netresearch/nr-vault:consequences@1.0)
-   [Related decisions](https://docs.typo3.org/permalink/netresearch/nr-vault:related-decisions@1.0)
-   [References](https://docs.typo3.org/permalink/netresearch/nr-vault:references@1.0)

## Status {#status}

Accepted

## Date {#date}

2026-01-03

## Context {#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 {#problem-statement}

How should nr-vault integrate with TYPO3's TCA system to transparently
encrypt sensitive fields while maintaining standard TYPO3 workflows?

## Decision drivers {#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 {#considered-options}

### Option 1: Custom field type {#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 {#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 {#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 {#decision}

We chose **custom renderType with DataHandler hooks** because:

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

## Implementation {#implementation}

### FormEngine element {#formengine-element}

**Classes/Form/Element/VaultSecretElement.php**

```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
    }
}
```

Registration in `ext_localconf.php`:

**ext_localconf.php**

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

### DataHandler hook {#datahandler-hook}

**Classes/Hook/DataHandlerHook.php**

```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. The shipped hook
    // delegates this to PendingSecretPersister, which rolls the field
    // value back and reports to the editor when the store is refused.
    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 {#flexform-hook}

Separate hook for FlexForm fields due to different data structure:

**Classes/Hook/FlexFormVaultHook.php**

```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
    }

    // Store the pending secrets once the record has its final UID
    public function processDatamap_afterDatabaseOperations(...): void;

    // Before delete: remove the secrets the FlexForm references
    public function processCmdmap_deleteAction(...): void;

    // After copy: re-key the copied FlexForm onto fresh identifiers
    public function processCmdmap_postProcess(...): void;
}
```

### TCA configuration {#tca-configuration}

Extensions add vault support with one line:

**Configuration/TCA/tx_myext_settings.php**

```php
'api_key' => [
    'label' => 'API Key',
    'config' => [
        'type' => 'input',
        'renderType' => 'vaultSecret',  // This one line
        'size' => 30,
    ],
],
```

Helper for common patterns:

**Using VaultFieldHelper**

```php
use Netresearch\NrVault\TCA\VaultFieldHelper;

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

### Data flow {#data-flow}

**TCA vault field data flow**

```text
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 delegates to
   PendingSecretPersister, which stores the secret in the vault
5. On refusal: the field value is rolled back, VaultFailureReporter tells
   the editor, and no success audit entry survives

Record Delete:
1. DataHandlerHook.processCmdmap_preProcess finds vault fields
2. Retrieves UUIDs from record
3. Asserts every field's delete gate BEFORE removing the first secret
4. Deletes corresponding vault secrets
5. On failure: processCmdmap cancels the record delete, so the record and
   its surviving secret stay together rather than orphaning either

Record Copy:
1. DataHandlerHook.processCmdmap_postProcess detects copy
2. Retrieves source secrets by UUID
3. Creates new secrets with new UUIDs for copied record
4. On failure: the secrets already cloned are deleted again and every
   vault field of the new record is blanked; both steps are best-effort,
   so a failed rollback delete leaves an orphaned clone and a failed
   blanking leaves the copy referencing the source record's secrets
```

### Runtime resolution {#runtime-resolution}

**Resolving secrets in application code**

```php
use Netresearch\NrVault\Utility\VaultFieldResolver;

// VaultFieldResolver is a DI service, not a static utility — inject it.
public function __construct(
    private readonly VaultFieldResolver $vaultFieldResolver,
) {}

// Resolve specific fields
$resolved = $this->vaultFieldResolver->resolveFields($record, ['api_key']);

// Auto-detect vault fields from TCA
$resolved = $this->vaultFieldResolver->resolveRecord('tx_myext_settings', $record);
```

## Consequences {#consequences}

### Positive {#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 {#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 {#risks}

-   Hook execution order conflicts with other extensions
-   FlexForm structure changes could break field detection

### Mitigation {#mitigation}

-   Use high priority for hooks
-   Comprehensive test coverage for FlexForm parsing
-   Clear documentation for resolution patterns

## Related decisions {#related-decisions}

-   [ADR-001: UUID v7 for secret identifiers](https://docs.typo3.org/permalink/netresearch/nr-vault:adr-001-uuid-v7@1.0) \- Identifier format for stored secrets

## References {#references}

-   [TYPO3 FormEngine Documentation](https://docs.typo3.org/m/typo3/reference-coreapi/main/en-us/ApiOverview/FormEngine/Index.html)
-   [TYPO3 DataHandler Hooks](https://docs.typo3.org/m/typo3/reference-coreapi/main/en-us/ApiOverview/Typo3CoreEngine/Database/Index.html)
