Separate Database

By default, AuditChain stores audit logs in your application's default database. For production systems — especially those with compliance requirements — a separate database connection is strongly recommended. Isolating audit data makes it significantly harder for a compromised application to tamper with the audit trail.

Why Use a Separate Connection?

Security isolation — If an attacker gains access to your application database, the audit trail is on a different server or database with different credentials. The audit logs survive the compromise and provide evidence for incident response.

Access control — The audit database user can be restricted to INSERT and SELECT only. No UPDATE, no DELETE. Even if someone obtains the credentials, they cannot modify or erase audit records at the database level.

Independent retention — Your application database might have its own backup and retention policies. A separate audit database lets you apply different policies — longer backups, more frequent snapshots, stricter access to dumps.

Performance isolation — High-volume audit logging does not compete with your application queries for the same connection pool, locks, or I/O.

Configuration

1. Define the Database Connection

Add an audit connection to config/database.php:

// config/database.php

'connections' => [

    // Your application database
    'mysql' => [
        'driver' => 'mysql',
        'host' => env('DB_HOST', '127.0.0.1'),
        'database' => env('DB_DATABASE', 'forge'),
        'username' => env('DB_USERNAME', 'forge'),
        'password' => env('DB_PASSWORD', ''),
        // ...
    ],

    // Dedicated audit database
    'audit' => [
        'driver' => 'mysql',
        'host' => env('AUDIT_DB_HOST', '127.0.0.1'),
        'port' => env('AUDIT_DB_PORT', '3306'),
        'database' => env('AUDIT_DB_DATABASE', 'audit'),
        'username' => env('AUDIT_DB_USERNAME', 'audit_user'),
        'password' => env('AUDIT_DB_PASSWORD', ''),
        'charset' => 'utf8mb4',
        'collation' => 'utf8mb4_unicode_ci',
        'prefix' => '',
        'strict' => true,
        'engine' => 'InnoDB',
    ],

],

2. Set Environment Variables

Add the audit database credentials to your .env:

AUDIT_DB_HOST=127.0.0.1
AUDIT_DB_PORT=3306
AUDIT_DB_DATABASE=audit_trail
AUDIT_DB_USERNAME=audit_user
AUDIT_DB_PASSWORD=secret

3. Point AuditChain to the Connection

Set the connection value in config/audit-chain.php:

'connection' => 'audit',

4. Run the Migration

When running migrations, specify the connection:

php artisan migrate --database=audit

Or, if you have already published the migration, the migration file will respect the configured connection automatically.

Database User Permissions

For maximum immutability, create a database user with minimal permissions:

-- MySQL
CREATE USER 'audit_user'@'%' IDENTIFIED BY 'strong-password';
GRANT SELECT, INSERT ON audit_trail.* TO 'audit_user'@'%';
FLUSH PRIVILEGES;
-- PostgreSQL
CREATE USER audit_user WITH PASSWORD 'strong-password';
GRANT SELECT, INSERT ON ALL TABLES IN SCHEMA public TO audit_user;

With these permissions:

  • AuditChain can write new audit log entries
  • AuditChain can read entries for verification and querying
  • Nobody can update or delete entries through this connection, even with direct SQL access

AuditChain already enforces immutability at the Eloquent layer by throwing a RuntimeException on update or delete. Database-level restrictions add a second layer of defense that survives application-level compromises.

Pruning with a Separate Database

The audit:prune command respects the configured connection automatically. However, if your audit database user only has INSERT and SELECT permissions, pruning will fail.

Two approaches:

Option A: Use a separate database user with DELETE permission for pruning. Run the prune command with a different connection or credentials.

Option B: Grant DELETE permission to the audit user but restrict it with a scheduled job that only runs through the Laravel scheduler. This is simpler but provides less isolation.

In practice, most teams go with Option B — the pruning command only deletes records older than the retention period, and the scheduler provides a controlled execution context.

Same Server, Different Database

You do not need a separate database server. A separate database on the same server still provides meaningful isolation:

AUDIT_DB_HOST=127.0.0.1
AUDIT_DB_DATABASE=myapp_audit
AUDIT_DB_USERNAME=audit_user
AUDIT_DB_PASSWORD=different-password

The key benefit is the separate user with restricted permissions. A different server adds network-level isolation but is not required.

Different Database Engine

The audit connection can use a different database engine than your application. For example, your application might use MySQL while the audit database uses PostgreSQL:

'audit' => [
    'driver' => 'pgsql',
    'host' => env('AUDIT_DB_HOST', '127.0.0.1'),
    'database' => env('AUDIT_DB_DATABASE', 'audit'),
    'username' => env('AUDIT_DB_USERNAME', 'audit_user'),
    'password' => env('AUDIT_DB_PASSWORD', ''),
    'schema' => 'public',
],

AuditChain uses standard SQL (no engine-specific features), so any Laravel-supported database works.

Note: SQLite does not support row-level locking (SELECT ... FOR UPDATE), so the sentinel table cannot serialize concurrent chain writes. AuditChain emits a Log::warning() once on first use to flag this. SQLite works well for development, testing, and low-concurrency production use, but for HasAuditTrail (full mode) under concurrent load, a database with row-level locking (MySQL, PostgreSQL) is recommended.