SHADOW AI

In Chapter 35, we created the implementation-ready database blueprint.
Now we select the concrete technology stack and prepare the database layer for ACAI.
The recommended architecture is:
ACAI Application
│
▼
Next.js / Backend
│
▼
ORM / Database Client
│
▼
PostgreSQL
│
├── Structured Data
│
└── Metadata
For retrieval:
Documents
↓
Chunks
↓
Embeddings
↓
Vector Search
The database architecture should remain modular so that individual infrastructure components can be replaced later if necessary.
For the primary relational database, use:
PostgreSQL
PostgreSQL is suitable for ACAI because the application requires:
Users
Relationships
Transactions
Indexes
Constraints
JSON metadata
Complex queries
Scalability
ACAI contains many strongly related entities.
For example:
User
↓
Project
↓
Conversation
↓
Message
and:
Project
↓
File
↓
Document
↓
Chunk
A relational database is well suited to these relationships.
A database abstraction layer should sit between the application and PostgreSQL.
Conceptually:
ACAI Services
↓
Repository Layer
↓
ORM
↓
PostgreSQL
An ORM can provide:
Schema Definition
Type Safety
Queries
Migrations
Relationships
Transactions
The exact ORM can be selected according to the project's implementation preferences.
The project should support multiple environments:
Development
Testing
Staging
Production
Each environment should have its own database configuration.
Example:
Development DB
Testing DB
Staging DB
Production DB
Do not use the production database for local development.
The application should obtain the database connection from an environment variable.
Conceptually:
DATABASE_URL
Example structure:
postgresql://USER:PASSWORD@HOST:PORT/DATABASE
The real production value must remain secret.
Local development can use:
.env
A safe template can use:
.env.example
The example file should contain placeholders rather than real credentials.
Example:
DATABASE_URL="postgresql://USER:PASSWORD@HOST:5432/acai"
This is documentation only.
The repository should normally ignore:
.env
.env.local
.env.production
unless a particular environment requires a non-secret configuration file.
Never commit:
Database passwords
API keys
Private tokens
Authentication secrets
Production credentials
Use consistent naming.
Recommended:
snake_case
Examples:
user_id
project_id
created_at
updated_at
deleted_at
conversation_id
Consistency becomes especially important as the schema grows.
Use plural table names:
users
projects
conversations
messages
files
documents
document_chunks
memories
agent_tasks
agent_steps
tool_calls
usage_records
audit_logs
This creates a predictable schema.
Every major table should have:
id
as its primary identifier.
Example:
users
---------
id
email
name
The exact underlying ID format can be UUID, ULID, or another suitable strategy.
Use explicit names:
user_id
project_id
conversation_id
file_id
document_id
task_id
step_id
Example:
messages.conversation_id
references:
conversations.id
Use:
created_at
updated_at
For lifecycle-specific events:
started_at
completed_at
deleted_at
This provides a consistent temporal model.
Conceptual SQL:
CREATE TABLE users (
id UUID PRIMARY KEY,
email TEXT NOT NULL UNIQUE,
name TEXT,
status TEXT NOT NULL,
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL
);
The actual production schema should use the database and ORM conventions selected for the project.
CREATE TABLE projects (
id UUID PRIMARY KEY,
user_id UUID NOT NULL,
name TEXT NOT NULL,
description TEXT,
status TEXT NOT NULL,
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL,
deleted_at TIMESTAMP,
FOREIGN KEY (user_id)
REFERENCES users(id)
);
CREATE TABLE conversations (
id UUID PRIMARY KEY,
user_id UUID NOT NULL,
project_id UUID,
title TEXT,
status TEXT NOT NULL,
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL,
deleted_at TIMESTAMP,
FOREIGN KEY (user_id)
REFERENCES users(id),
FOREIGN KEY (project_id)
REFERENCES projects(id)
);
CREATE TABLE messages (
id UUID PRIMARY KEY,
conversation_id UUID NOT NULL,
role TEXT NOT NULL,
content TEXT NOT NULL,
model TEXT,
provider TEXT,
input_tokens INTEGER,
output_tokens INTEGER,
created_at TIMESTAMP NOT NULL,
FOREIGN KEY (conversation_id)
REFERENCES conversations(id)
);
CREATE TABLE files (
id UUID PRIMARY KEY,
user_id UUID NOT NULL,
project_id UUID,
name TEXT NOT NULL,
mime_type TEXT NOT NULL,
size BIGINT NOT NULL,
storage_key TEXT NOT NULL,
status TEXT NOT NULL,
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL,
deleted_at TIMESTAMP,
FOREIGN KEY (user_id)
REFERENCES users(id),
FOREIGN KEY (project_id)
REFERENCES projects(id)
);
CREATE TABLE documents (
id UUID PRIMARY KEY,
file_id UUID NOT NULL,
project_id UUID,
status TEXT NOT NULL,
page_count INTEGER,
text_length INTEGER,
parser TEXT,
processing_version TEXT,
error_code TEXT,
started_at TIMESTAMP,
completed_at TIMESTAMP,
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL,
FOREIGN KEY (file_id)
REFERENCES files(id),
FOREIGN KEY (project_id)
REFERENCES projects(id)
);
CREATE TABLE document_chunks (
id UUID PRIMARY KEY,
document_id UUID NOT NULL,
project_id UUID,
content TEXT NOT NULL,
chunk_index INTEGER NOT NULL,
token_count INTEGER,
page_number INTEGER,
section TEXT,
created_at TIMESTAMP NOT NULL,
FOREIGN KEY (document_id)
REFERENCES documents(id)
);
Recommended constraint:
UNIQUE(document_id, chunk_index)
This prevents duplicate chunk positions within one document.
CREATE TABLE memories (
id UUID PRIMARY KEY,
user_id UUID NOT NULL,
project_id UUID,
type TEXT NOT NULL,
content TEXT NOT NULL,
importance REAL,
confidence REAL,
source TEXT,
status TEXT NOT NULL,
last_used_at TIMESTAMP,
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL,
FOREIGN KEY (user_id)
REFERENCES users(id),
FOREIGN KEY (project_id)
REFERENCES projects(id)
);
CREATE TABLE agent_tasks (
id UUID PRIMARY KEY,
user_id UUID NOT NULL,
project_id UUID,
conversation_id UUID,
goal TEXT NOT NULL,
status TEXT NOT NULL,
current_step INTEGER,
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL,
started_at TIMESTAMP,
completed_at TIMESTAMP,
FOREIGN KEY (user_id)
REFERENCES users(id),
FOREIGN KEY (project_id)
REFERENCES projects(id),
FOREIGN KEY (conversation_id)
REFERENCES conversations(id)
);
CREATE TABLE agent_steps (
id UUID PRIMARY KEY,
task_id UUID NOT NULL,
step_number INTEGER NOT NULL,
action TEXT NOT NULL,
status TEXT NOT NULL,
input JSONB,
output JSONB,
started_at TIMESTAMP,
completed_at TIMESTAMP,
created_at TIMESTAMP NOT NULL,
FOREIGN KEY (task_id)
REFERENCES agent_tasks(id),
UNIQUE(task_id, step_number)
);
CREATE TABLE tool_calls (
id UUID PRIMARY KEY,
task_id UUID NOT NULL,
step_id UUID,
tool_name TEXT NOT NULL,
input JSONB,
output JSONB,
status TEXT NOT NULL,
started_at TIMESTAMP,
completed_at TIMESTAMP,
created_at TIMESTAMP NOT NULL,
FOREIGN KEY (task_id)
REFERENCES agent_tasks(id),
FOREIGN KEY (step_id)
REFERENCES agent_steps(id)
);
CREATE TABLE usage_records (
id UUID PRIMARY KEY,
user_id UUID NOT NULL,
project_id UUID,
request_type TEXT NOT NULL,
model TEXT,
provider TEXT,
input_tokens INTEGER,
output_tokens INTEGER,
tool_calls INTEGER,
duration INTEGER,
created_at TIMESTAMP NOT NULL,
FOREIGN KEY (user_id)
REFERENCES users(id),
FOREIGN KEY (project_id)
REFERENCES projects(id)
);
CREATE TABLE audit_logs (
id UUID PRIMARY KEY,
user_id UUID,
action TEXT NOT NULL,
resource_type TEXT,
resource_id UUID,
result TEXT,
metadata JSONB,
created_at TIMESTAMP NOT NULL,
FOREIGN KEY (user_id)
REFERENCES users(id)
);
Initial indexes can be created for common access patterns.
CREATE INDEX idx_projects_user_id
ON projects(user_id);
CREATE INDEX idx_conversations_user_id
ON conversations(user_id);
CREATE INDEX idx_messages_conversation_id_created_at
ON messages(conversation_id, created_at);
CREATE INDEX idx_files_project_id
ON files(project_id);
CREATE INDEX idx_documents_file_id
ON documents(file_id);
CREATE INDEX idx_chunks_document_id
ON document_chunks(document_id);
CREATE INDEX idx_memories_user_id
ON memories(user_id);
CREATE INDEX idx_agent_tasks_user_id_status
ON agent_tasks(user_id, status);
CREATE INDEX idx_agent_steps_task_id
ON agent_steps(task_id);
CREATE INDEX idx_tool_calls_task_id
ON tool_calls(task_id);
CREATE INDEX idx_usage_user_id_created_at
ON usage_records(user_id, created_at);
CREATE INDEX idx_audit_logs_user_id_created_at
ON audit_logs(user_id, created_at);
These indexes should later be validated using actual query performance.
Some ACAI entities need flexible structured data.
PostgreSQL provides:
JSONB
This is useful for:
Tool inputs
Tool outputs
Agent metadata
Audit metadata
Configuration
Example:
input JSONB
However, frequently queried fields should not be hidden unnecessarily inside JSON.
Use normal columns for:
user_id
project_id
status
created_at
model
provider
Use JSONB for:
dynamic tool parameters
optional metadata
variable tool output
This produces a balanced schema.
The database schema should be created using migrations.
Example:
migrations/
│
├── 001_create_users
├── 002_create_projects
├── 003_create_conversations
├── 004_create_messages
├── 005_create_files
├── 006_create_documents
├── 007_create_chunks
├── 008_create_memories
├── 009_create_agent_tasks
├── 010_create_agent_steps
├── 011_create_tool_calls
├── 012_create_usage_records
└── 013_create_audit_logs
A migration should be:
Versioned
Repeatable in a new environment
Reviewable
Traceable
Never depend on undocumented manual production changes.
Development:
Modify Schema
↓
Generate Migration
↓
Review Migration
↓
Apply Migration
↓
Test
Production:
Approved Migration
↓
Backup / Safety Check
↓
Apply Migration
↓
Verify
↓
Monitor
The database implementation can be organized as:
src/
└── server/
└── database/
├── client
├── schema
├── migrations
├── repositories
└── seed
The exact paths can vary depending on the ACAI codebase.
Conceptually:
ProjectRepository
│
├── create()
├── findById()
├── findByUserId()
├── update()
└── softDelete()
Then:
ProjectService
↓
ProjectRepository
↓
Database
Without a repository layer:
Route
↓
Raw SQL
↓
Database
can spread database-specific logic across the entire application.
With a repository:
Route
↓
Service
↓
Repository
↓
Database
database operations remain centralized.
Transactions should be exposed where multiple changes must remain consistent.
Example:
createAgentTask()
may perform:
Create Task
Create Initial Step
Create Audit Log
inside one transaction.
Database errors should not be returned directly to end users.
Instead:
Database Error
↓
Repository
↓
Service Error
↓
API Error
The API should expose a safe, meaningful error response.
The backend should reuse database connections efficiently.
Avoid:
Request
↓
Create new database connection
↓
Query
↓
Destroy connection
for every request.
Instead, use the connection management mechanism provided by the selected database client.
For local development, use a dedicated PostgreSQL instance.
Conceptually:
Windows
↓
PostgreSQL
↓
ACAI Development Database
Alternatively, a containerized PostgreSQL environment can be used.
The important rule is isolation from production.
Automated tests should not modify the production database.
Use:
ACAI_TEST_DB
or an isolated database/container.
Example:
Tests
↓
Test Database
↓
Cleanup
Before production:
Development
↓
Testing
↓
Staging
↓
Production
The staging environment should be sufficiently similar to production to detect deployment issues.
Production should include:
Secure Credentials
Encrypted Connections
Backups
Monitoring
Access Control
Migration Management
Recovery Plan
Only authorized backend infrastructure should have database access.
The relational database does not necessarily need to perform all vector-search operations.
Architecture:
PostgreSQL
│
└── Document Metadata
│
▼
Vector Storage
│
└── Embeddings
The exact vector technology can be selected later.
Every vector record should be traceable back to:
vector
↓
chunk
↓
document
↓
file
↓
project
↓
user
This is essential for authorization.
A search result must not be returned merely because its vector is similar; it must also belong to an accessible project/user scope.
The security architecture should look like:
Browser
│
▼
API
│
▼
Authorization
│
▼
Service
│
▼
Repository
│
▼
Database
The browser should never connect directly to the primary database.
[✓] PostgreSQL selected
[✓] ORM/database abstraction planned
[✓] Environment configuration defined
[✓] User schema defined
[✓] Project schema defined
[✓] Conversation schema defined
[✓] Message schema defined
[✓] File schema defined
[✓] Document schema defined
[✓] Chunk schema defined
[✓] Memory schema defined
[✓] Agent task schema defined
[✓] Agent step schema defined
[✓] Tool call schema defined
[✓] Usage schema defined
[✓] Audit schema defined
[✓] Foreign keys defined
[✓] Initial indexes defined
[✓] Migration strategy defined
[✓] Repository layer defined
[✓] Transaction strategy defined
[✓] Environment separation defined
[✓] Security boundary defined
The resulting database stack is:
ACAI APPLICATION
│
▼
SERVICE LAYER
│
▼
REPOSITORY LAYER
│
▼
ORM / CLIENT
│
▼
POSTGRESQL
│
┌───────────┴───────────┐
▼ ▼
Structured Data Metadata
RAG extends the architecture:
Documents
↓
Chunks
↓
Embeddings
↓
Vector Storage
And files are handled separately:
Uploaded File
↓
Object Storage
+
File Metadata
↓
PostgreSQL
The database technology and implementation blueprint are now established.
The next chapter will move from schema design into the actual ACAI backend database connection, ORM configuration, migrations, seed system, and first working database operations.
END OF CHAPTER 36
Comments
Post a Comment