SHADOW AI

Chapter 27 established the account and persistence foundation.
Now we build the actual working workspace around that foundation.
The target is:
LOGIN
↓
DASHBOARD
↓
PROJECTS
↓
CONVERSATIONS
↓
CHAT HISTORY
↓
FILE UPLOAD
↓
SECURE STORAGE
↓
DATABASE RECORD
After this chapter, ACAI should feel like a real AI workspace rather than only a chat application.
This chapter introduces:
[✓] Dashboard
[✓] Project list
[✓] Project creation
[✓] Conversation list
[✓] Conversation creation
[✓] Conversation history
[✓] Delete/archive controls
[✓] File upload UI
[✓] File validation
[✓] Object storage architecture
[✓] File database records
[✓] User ownership
[✓] Secure download/access
[✓] Upload limits
[✓] Upload error handling
The architecture becomes:
ACAI
│
┌────────────┼────────────┐
▼ ▼ ▼
DASHBOARD CHAT FILES
│ │ │
▼ ▼ ▼
PROJECTS CONVERSATIONS STORAGE
│ │
▼ ▼
MESSAGES DATABASE
The dashboard is the user's main workspace.
It should answer immediately:
What projects do I have?
What conversations did I use recently?
What files have I uploaded?
What can I do next?
A basic layout:
┌───────────────────────────────────────────────┐
│ ACAI Profile │
├───────────────┬───────────────────────────────┤
│ Dashboard │ Welcome back │
│ Projects │ │
│ Conversations │ [New Project] [New Chat] │
│ Files │ │
│ Settings │ Recent Projects │
│ │ Recent Conversations │
│ │ Recent Files │
└───────────────┴───────────────────────────────┘
The dashboard should be private:
/dashboard
Flow:
OPEN /dashboard
↓
SESSION CHECK
↓
┌─────┴─────┐
▼ ▼
VALID INVALID
▼ ▼
DASHBOARD LOGIN
Do not load everything directly from the browser using unrestricted database queries.
Use controlled server-side endpoints or server-side data access.
Conceptually:
GET /api/dashboard
could return normalized information such as:
{
"projects": [],
"recentConversations": [],
"recentFiles": []
}
The exact endpoint structure can be different depending on the implementation.
The user clicks:
+ New Project
A small form appears:
Project Name
Description
[Cancel] [Create]
Flow:
FORM
↓
VALIDATE
↓
AUTHENTICATED USER
↓
CREATE PROJECT
↓
DATABASE
↓
REFRESH DASHBOARD
Validate on both sides.
Frontend:
empty?
too long?
invalid?
Server:
empty?
too long?
authorized?
The server remains the final authority.
When creating:
project.userId = authenticatedUser.id
Never accept an arbitrary userId from the browser.
Incorrect:
{
"name": "My Project",
"userId": "someone-else"
}
Correct architecture:
Browser
↓
name only
↓
Server
↓
session.userId
↓
database
Dashboard:
PROJECTS
┌──────────────────────────────┐
│ Marketing AI │
│ 12 conversations │
│ Updated recently │
└──────────────────────────────┘
┌──────────────────────────────┐
│ Research │
│ 8 conversations │
│ Updated yesterday │
└──────────────────────────────┘
Only projects belonging to the authenticated user should be returned.
A project can eventually have:
Project
├── Conversations
├── Files
├── Generations
└── Settings
This gives ACAI a workspace model.
The dashboard can show:
RECENT CONVERSATIONS
Explain quantum computing
Today
Research assistant
Yesterday
Marketing plan
Aug 29
Clicking a conversation opens:
/chat/[conversationId]
The flow:
NEW CHAT
↓
CREATE CONVERSATION
↓
ASSOCIATE USER
↓
ASSOCIATE PROJECT
↓
DATABASE
↓
OPEN CHAT
At creation, a conversation can have:
New Conversation
Later, ACAI can automatically generate a title from the first message.
Example:
User:
Explain how neural networks work.
Generated title:
Neural Network Basics
This title-generation feature can be added after the basic persistence works.
Inside chat:
CONVERSATIONS
+ New Chat
Today
├── AI Architecture
├── Research Notes
└── Product Plan
Yesterday
├── Python Help
└── Marketing Ideas
Selecting one loads its messages.
A user may eventually delete a conversation.
Flow:
DELETE
↓
AUTH
↓
OWNERSHIP CHECK
↓
DATABASE
↓
DELETE / SOFT DELETE
For important systems, consider soft deletion:
deletedAt
instead of immediately destroying every record.
An alternative is:
ACTIVE
ARCHIVED
This lets the user hide old conversations without immediately deleting them.
Now we introduce files.
Important distinction:
DATABASE
≠
FILE STORAGE
The database stores information about a file.
Object storage stores the actual file bytes.
Architecture:
USER
↓
UPLOAD
↓
OBJECT STORAGE
↓
FILE RECORD
↓
DATABASE
A conceptual model:
File
--------------------------------
id
userId
projectId
name
mimeType
size
storageKey
status
createdAt
updatedAt
Optional future fields:
checksum
width
height
duration
pageCount
metadata
processingError
Do not store every file using only its original filename.
For example:
report.pdf
is not a safe unique storage identity.
Instead use an application-generated storage key:
users/{userId}/projects/{projectId}/files/{fileId}
This creates a predictable ownership boundary.
Suppose two users upload:
resume.pdf
Both files have the same filename.
But their storage identities are different:
users/A/.../file-001
users/B/.../file-002
The database keeps the display name:
resume.pdf
while storage uses a unique key.
The first file interface:
┌──────────────────────────────────────┐
│ │
│ Drag & Drop Files Here │
│ │
│ or │
│ │
│ [Choose Files] │
│ │
└──────────────────────────────────────┘
Below:
Allowed file types
Maximum size
Upload progress
Before upload:
FILE
↓
TYPE CHECK
↓
SIZE CHECK
↓
NAME CHECK
↓
ACCEPT / REJECT
Possible restrictions:
maximum file size
maximum number of files
allowed MIME types
allowed extensions
The exact limits should be configuration-driven.
A browser may report:
application/pdf
but that should not be treated as absolute proof of the file's actual contents.
For higher-security workflows:
upload
↓
server-side validation
↓
content inspection
↓
accept
For small prototypes:
Browser
↓
Application Server
↓
Storage
For scalable production:
Browser
↓
Request upload permission
↓
Signed upload URL
↓
Object Storage
The second approach avoids sending large files unnecessarily through the application server.
Conceptual flow:
BROWSER
│
│ 1. request upload
▼
ACAI SERVER
│
│ 2. validate user/file
▼
SIGNED URL
│
│ 3. upload directly
▼
OBJECT STORAGE
The server controls who receives permission and what resource they can upload.
A useful architecture is:
POST /api/files/upload-init
Request:
{
"name": "report.pdf",
"size": 1200000,
"mimeType": "application/pdf"
}
Server:
AUTH
↓
VALIDATE
↓
CREATE FILE ID
↓
CREATE STORAGE KEY
↓
CREATE UPLOAD PERMISSION
↓
RETURN
After the browser uploads:
OBJECT STORAGE
↓
UPLOAD COMPLETE
↓
POST /api/files/complete
↓
SERVER VERIFY
↓
FILE STATUS = READY
Use explicit states.
Example:
UPLOADING
PROCESSING
READY
FAILED
DELETED
Flow:
UPLOADING
↓
PROCESSING
↓
READY
If something fails:
PROCESSING
↓
FAILED
A file may exist in storage but not yet be ready for AI processing.
Therefore:
FILE EXISTS
≠
FILE READY FOR RAG
The UI should communicate the actual state.
Project page:
FILES
report.pdf
1.2 MB
Ready
research.docx
4.7 MB
Processing
image.png
2.1 MB
Ready
Never create a public permanent URL for private user files unless the file is intentionally public.
Preferred flow:
USER
↓
DOWNLOAD
↓
AUTH
↓
OWNERSHIP CHECK
↓
TEMPORARY SIGNED ACCESS
↓
STORAGE
A file belongs to:
userId
projectId
When requested:
requested file
↓
file.userId === session.userId
↓
YES → continue
NO → deny
Deletion can involve two systems:
DATABASE
+
OBJECT STORAGE
Therefore:
DELETE REQUEST
↓
AUTHORIZATION
↓
STORAGE DELETE
↓
DATABASE UPDATE
The implementation should be designed so partial failures can be recovered or retried.
An orphan can occur when:
storage upload succeeds
BUT
database write fails
or:
database record exists
BUT
storage deletion fails
Production systems need cleanup/reconciliation jobs.
This chapter only establishes the upload foundation.
The next processing layer will be:
FILE
↓
EXTRACT CONTENT
↓
NORMALIZE
↓
CHUNK
↓
EMBED
↓
VECTOR STORE
Do not mix all of that into the initial upload request.
Large files can take time to process.
Bad architecture:
UPLOAD
↓
WAIT 5 MINUTES
↓
RETURN
Better:
UPLOAD
↓
QUEUE JOB
↓
RETURN QUICKLY
↓
WORKER PROCESSES FILE
Later:
FILE UPLOAD
↓
DATABASE
↓
JOB QUEUE
↓
WORKER
↓
TEXT EXTRACTION
↓
CHUNKING
↓
EMBEDDINGS
↓
VECTOR DATABASE
This will be developed in later chapters.
Uploaded files should be treated as untrusted input.
Security measures should include:
size limits
type validation
authorization
safe storage keys
access controls
malware/security scanning where appropriate
processing isolation
Do not execute uploaded files.
A user may upload:
../../secret.txt
or filenames containing unusual characters.
Never directly use an uploaded filename as a filesystem path.
Store:
displayName
separately from:
storageKey
Never construct local or storage paths directly from uncontrolled input.
Incorrect concept:
storage/user-input-filename
Correct:
server-generated fileId
+
server-generated storageKey
There should be several levels:
Frontend limit
API limit
Storage limit
Processing limit
The backend/storage layer remains authoritative.
A project may eventually have:
maximum storage
maximum number of files
maximum file size
maximum processing jobs
Example:
PROJECT
↓
QUOTA CHECK
↓
Enough capacity?
├── YES → upload
└── NO → reject
Similarly:
USER
↓
STORAGE USAGE
↓
QUOTA
This becomes useful for free/pro/enterprise plans.
The dashboard can display:
Storage
1.8 GB / 10 GB
████████░░░░
The exact UI and limits depend on the application's billing model.
Dashboard:
RECENT FILES
report.pdf
2 minutes ago
research.docx
Yesterday
presentation.pptx
Aug 28
Clicking a file can open the file details or project context.
A file detail view can show:
Name
Type
Size
Uploaded
Status
Project
Processing status
Later:
Pages
Chunks
Embeddings
AI indexing status
At this point, a project can become:
┌────────────────────────────────────────────┐
│ Research Project │
├─────────────┬──────────────────────────────┤
│ Overview │ Recent Activity │
│ Chat │ │
│ Files │ Files │
│ Generations │ Conversations │
│ Settings │ │
└─────────────┴──────────────────────────────┘
A project-specific chat should automatically associate the conversation:
PROJECT
↓
NEW CHAT
↓
CONVERSATION.projectId
This makes project context possible later.
Files should also belong to the project:
PROJECT
├── CHAT
├── CHAT
├── FILE
├── FILE
└── FILE
Then RAG can later use project-specific documents.
If a user has:
Project A
Project B
documents from Project B should not automatically become context for Project A.
The future retrieval layer should respect:
userId
projectId
permissions
Eventually:
USER
↓
QUESTION
↓
CURRENT PROJECT
↓
SEARCH PROJECT FILES
↓
RELEVANT DOCUMENTS
↓
AI
This is the bridge from Chapter 28 to RAG.
When a file becomes ready:
FILE READY
↓
CREATE PROCESSING JOB
↓
QUEUE
The queue might contain:
jobId
fileId
projectId
userId
jobType
status
createdAt
A worker processes:
JOB
↓
DOWNLOAD FILE
↓
EXTRACT CONTENT
↓
NORMALIZE
↓
CHUNK
↓
STORE RESULT
The worker should not trust client-provided ownership information.
It should use database records.
If processing fails temporarily:
FAILED
↓
RETRY
But permanently invalid files should eventually become:
FAILED_PERMANENTLY
and provide a meaningful error state.
After uploading:
UPLOAD
↓
STATUS = PROCESSING
↓
UI UPDATE
↓
STATUS = READY
The frontend can use:
polling
server-sent events
websocket
revalidation
depending on the final architecture.
Do not add real-time infrastructure before it is actually needed.
A possible structure:
/api
├── auth
├── dashboard
├── projects
├── conversations
├── chat
└── files
Within files:
POST /api/files/upload-init
POST /api/files/complete
GET /api/files
GET /api/files/[id]
DELETE /api/files/[id]
The exact routing can be changed during implementation.
Conceptually:
Endpoint Auth
------------------------------------------------
GET /dashboard YES
POST /projects YES
GET /projects/:id YES
POST /conversations YES
GET /conversations/:id YES
POST /chat YES
POST /files/upload-init YES
POST /files/complete YES
GET /files/:id YES
DELETE /files/:id YES
Public endpoints should be explicitly identified rather than assumed.
Never assume:
button hidden
=
permission denied
For example, hiding:
Delete Project
does not protect the API.
The server must still enforce:
authentication
authorization
ownership
A clean request flow:
REQUEST
↓
AUTH
↓
SCHEMA VALIDATION
↓
AUTHORIZATION
↓
BUSINESS LOGIC
↓
DATABASE / STORAGE
↓
RESPONSE
This pattern should become standard across ACAI.
Keep API errors consistent.
Conceptually:
{
"error": {
"code": "FILE_TOO_LARGE",
"message": "The uploaded file exceeds the allowed size."
}
}
The internal stack trace remains server-side.
Instead of:
500 Internal Server Error
show:
Unable to upload this file.
Please check the file size and try again.
Technical details can remain in developer logs.
Every major operation should have a state:
idle
loading
success
error
For files:
selecting
uploading
processing
ready
failed
This makes the interface predictable.
A new user may have no projects.
Do not show a blank screen.
Show:
No projects yet.
Create your first project to get started.
[Create Project]
For conversations:
No conversations yet.
[Start New Chat]
For files:
No files uploaded yet.
[Upload File]
The dashboard should work on:
Desktop
Tablet
Mobile
Desktop:
Sidebar + Main Workspace
Mobile:
Top bar
Drawer
Main Workspace
Important controls should have:
labels
keyboard access
visible focus
sensible contrast
screen-reader-friendly names
File upload should not rely only on drag-and-drop.
There must be:
Choose Files
as an alternative.
Do not load:
all conversations
all files
all messages
at once.
Use:
pagination
cursor pagination
search
lazy loading
where appropriate.
Instead of:
SELECT ALL MESSAGES
use a bounded result.
Conceptually:
Latest 50 messages
↓
Load older messages
This becomes important for long-running conversations.
Similarly:
Files 1–25
then:
Next
or infinite scrolling.
The project workspace will eventually support:
Search conversations
Search files
Search messages
For now, basic database filtering is sufficient.
Semantic search belongs to the RAG stage.
For production, important actions can be recorded:
LOGIN
PROJECT_CREATED
CONVERSATION_CREATED
FILE_UPLOADED
FILE_DELETED
This becomes useful for:
security
debugging
enterprise auditing
The system now looks like:
USER
│
▼
AUTH SESSION
│
▼
DASHBOARD
│
┌────────────┼────────────┐
▼ ▼ ▼
PROJECTS CHAT FILES
│ │ │
▼ ▼ ▼
DATABASE DATABASE STORAGE
│ │ │
└──────┬─────┘ │
▼ ▼
USER FILE RECORD
│
▼
PROCESSING
A user can now do:
1. Open ACAI
↓
2. Sign up
↓
3. Login
↓
4. Open Dashboard
↓
5. Create Project
↓
6. Open Project
↓
7. Create Conversation
↓
8. Send AI message
↓
9. Refresh page
↓
10. Conversation remains
↓
11. Upload document
↓
12. File stored
↓
13. File status becomes PROCESSING
↓
14. Later → READY
That is the foundation of a true AI workspace.
[ ] New account
[ ] Login
[ ] Logout
[ ] Protected dashboard
[ ] Invalid session rejected
[ ] Create
[ ] List
[ ] Open
[ ] Rename
[ ] Archive/delete
[ ] Ownership enforcement
[ ] Create
[ ] Open
[ ] Send message
[ ] Persist message
[ ] Reload history
[ ] Ownership enforcement
[ ] Upload
[ ] Validate size
[ ] Validate type
[ ] Store
[ ] Create DB record
[ ] Show processing state
[ ] Download securely
[ ] Delete
[ ] Ownership enforcement
Create:
USER A
USER B
Then test:
A → A project ✓
A → A conversation ✓
A → A files ✓
A → B project ✗
A → B conversation ✗
A → B files ✗
This must work even if User A manually changes IDs in requests.
Test:
Invalid file
Too-large file
Interrupted upload
Expired session
Missing project
Wrong conversation ID
Wrong file ID
Database unavailable
Storage unavailable
AI provider unavailable
The application should fail gracefully.
Before calling the file system production-ready:
[ ] Storage access private
[ ] Signed access implemented
[ ] Server authorization
[ ] File validation
[ ] Size limits
[ ] Quotas
[ ] Cleanup strategy
[ ] Processing status
[ ] Retry strategy
[ ] Monitoring
[ ] Backup/recovery plan
Do not immediately add:
10 vector databases
20 AI providers
complex agent loops
distributed microservices
Kubernetes
large-scale billing
The correct order is:
CORE
↓
STORAGE
↓
PROCESSING
↓
RAG
↓
AGENT
↓
ADVANCED AI
↓
SCALE
At this point:
ACAI
│
├── Authentication
│
├── Dashboard
│
├── Projects
│
├── Conversations
│
├── Messages
│
└── Files
│
└── Secure Storage
The system has become a real AI workspace platform foundation.
Uploaded files are currently just files.
The AI does not yet understand them.
To make ACAI capable of answering:
"What does my uploaded research paper say?"
we need:
FILE
↓
TEXT EXTRACTION
↓
CLEANING
↓
CHUNKING
↓
EMBEDDINGS
↓
VECTOR DATABASE
↓
RETRIEVAL
↓
AI
That is RAG — Retrieval-Augmented Generation.
The next architecture will be:
UPLOADED FILE
│
▼
FILE PROCESSOR
│
┌───────────┴───────────┐
▼ ▼
PDF/DOCX IMAGE
│ │
▼ ▼
TEXT EXTRACTION OCR / VISION
│ │
└───────────┬───────────┘
▼
NORMALIZED TEXT
│
▼
CHUNKS
│
▼
EMBEDDINGS
│
▼
VECTOR DATABASE
│
▼
RAG
│
▼
AI
Then ACAI will move from:
"AI that can chat"
to:
"AI that can understand and retrieve information from the user's authorized documents."
[✓] Dashboard architecture
[✓] Project management
[✓] Conversation history
[✓] Project ownership
[✓] Conversation ownership
[✓] File database model
[✓] Object storage architecture
[✓] Upload validation
[✓] Secure storage keys
[✓] Signed upload architecture
[✓] Secure download architecture
[✓] File status system
[✓] File deletion strategy
[✓] Quota foundation
[✓] Processing queue foundation
[✓] Worker architecture
[✓] Pagination strategy
[✓] Empty states
[✓] Loading states
[✓] Error handling
[✓] User-isolation testing
[✓] Production security checklist
The complete ACAI workspace flow is now:
┌───────────────┐
│ USER │
└───────┬───────┘
│
▼
┌───────────────┐
│ AUTH / SESSION│
└───────┬───────┘
│
▼
┌───────────────┐
│ DASHBOARD │
└───────┬───────┘
│
┌─────────────┼─────────────┐
▼ ▼ ▼
PROJECTS CHAT/MSG FILES
│ │ │
▼ ▼ ▼
DATABASE DATABASE STORAGE
│
▼
FILE RECORD
│
▼
PROCESSING
The next major transformation is to take that stored file and turn it into AI-searchable knowledge.
END OF CHAPTER 28
Comments
Post a Comment