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 aLog::warning()once on first use to flag this. SQLite works well for development, testing, and low-concurrency production use, but forHasAuditTrail(full mode) under concurrent load, a database with row-level locking (MySQL, PostgreSQL) is recommended.