Spacerr uses Vercel Blob for file storage and the database for file records, ownership, and relationships. Private user files stay behind authenticated API routes. Public blog cover images use a separate public Blob token.
What It Stores
Storage is used for:
- Chat attachments.
- Long pasted text saved as files.
- Project source files.
- Generated images from AI tools.
- Profile images.
- Blog cover images.
- Library files shown in the dashboard file library.
Private And Public Stores
Use one private Blob store for user owned files and one public Blob store for public marketing or content assets.
BLOB_READ_WRITE_TOKENbelongs to the private store. It is used for chat attachments, project source files, generated images, profile images, and the private library.BLOB_PUBLIC_READ_WRITE_TOKENbelongs to the public store. It is used for blog cover images and other public assets.
The setup guide covers where to create both stores in Vercel and where to paste the tokens.
Source Records
The app uses two database tables for private AI workspace files. The split is intentional, because files can come from two different user actions.
Message Sources
message_source stores files that are part of a chat message. These records always have a chatId and a messageId.
Use message_source when the user sends a file through the chat input, pastes long content that is converted to a file, or receives a generated image inside a chat message. The file exists because a message contains it.
Message sources are scoped by the chat they belong to:
- If the chat has no
projectId, its message sources appear in Library. - If the chat has a
projectId, its message sources appear in that project’s Sources tab. - If the chat is moved into a project, those message sources become project scoped.
- If the chat is moved out of a project, those message sources become Library scoped again.
When a message source is deleted from Library or from project sources, the app uses messageId to remove that file part from the chat history as well. This prevents deleted files from leaving broken attachment chips in old messages.
Project Sources
project_source stores files uploaded directly to a project’s Sources tab. These records have a projectId, but no chatId and no messageId.
Use project_source when the user is adding reference material to a project, not sending a chat message. Uploading a project source should not create a chat, should not create a message, and should not show in the global Library.
Project sources are only visible inside their owning project. Deleting a project source removes the source record and its Blob file, but it does not edit chat history because no chat message was created for that upload.
Both tables store Blob metadata such as pathname, filename, mediaType, and sizeBytes. Blob stores bytes, while the database decides ownership, scope, and relationships.
Source Summaries And File Discovery
Source records also store lightweight AI discovery fields:
summarycontentTextsummaryStatus
When a chat attachment or project source is saved, a background job tries to extract readable text and create a short summary. Supported documents such as text files, JSON, PDFs, Word documents, and spreadsheets can produce searchable text. Images and unsupported binary files still keep filename, media type, size, and ownership metadata.
These fields let the assistant find files later without loading every Blob file into every message. The search tool checks Library files, project sources, and files from project chats. It searches filenames, summaries, and extracted content, then returns limited human readable results.
The first message that uploads a file can send the actual file content to the model. Later messages do not silently fetch the same Blob again. Older file parts are converted into lightweight references, and the assistant uses source summaries or extracted text first. If the user asks about a file again and more detail is needed, the assistant can read that user owned source through the source tool.
Chat Attachment Uploads
Chat attachments are uploaded through src/app/api/chats/attachments/route.ts.
const uploaded = await put(pathname, file, {
access: "private",
addRandomSuffix: true,
contentType: file.type || "application/octet-stream",
})
return NextResponse.json({
pathname: uploaded.pathname,
sizeBytes: file.size,
})Reads and deletes verify that the requested pathname belongs to the current user before touching Blob storage.
if (!isOwnChatAttachmentPathname(userId, pathname)) {
return NextResponse.json({ error: "Forbidden" }, { status: 403 })
}This keeps private file access user scoped even when a Blob pathname is known.
Attachment reads currently return Cache-Control: private, max-age=60. After a Blob file is deleted, a browser may still reuse a recently cached attachment response for up to 60 seconds. During that window, clicking an old link can appear to download the deleted file even though Blob deletion has already run.
Project Source Uploads
Project source files use the same private Blob pattern. The project repository creates file records and connects them to project context so the AI assistant can use uploaded source material when answering.
src/features/ai/chat/components/project-workspace
src/features/ai/chat/repositories/project.repository.ts
src/app/api/chats/projects/[projectId]/sourcesKeep source file ownership checks in the repository and route layer. Do not let the client decide whether a file belongs to the current user.
Project source uploads create project_source rows, not message_source rows. That is what keeps source tab uploads out of chat history and out of Library.
Generated Images
AI generated images are stored as private files and returned through app controlled URLs. This lets the product keep generated assets in the user library instead of relying on temporary provider URLs.
src/features/ai/chat/tools/chat-generate-image-tool.server.ts
src/features/library/repositories/library.repository.tsThe library can list, preview, download, and delete generated images with the same ownership rules as normal uploaded files.
Blog Cover Images
Blog cover images are public because they are rendered on public blog pages and social previews.
const uploaded = await put(`blog-covers/${Date.now()}-${sanitizeFilename(file.name)}`, file, {
access: "public",
addRandomSuffix: true,
token: ServerEnv.BLOB_PUBLIC_READ_WRITE_TOKEN,
})
return NextResponse.json({
imageUrl: uploaded.url,
})Only admin and moderator users can upload blog cover images.
File Records
Vercel Blob stores the files. The database stores metadata such as owner, pathname, media type, and relationships to projects, chats, or messages.
Keep file ownership in the database. Use Blob for storage, not authorization.
Where To Customize
Use these files first:
src/app/api/chats/attachments/route.tsfor chat attachment upload, read, download, and delete.src/app/api/chats/projects/[projectId]/sources/route.tsfor project source uploads.src/app/api/account/profile-image/route.tsfor profile image upload and reads.src/app/api/blog/upload-cover/route.tsfor public blog cover uploads.src/features/ai/chat/constants/chat-attachment.constants.tsfor upload limits.src/features/ai/chat/utils/chat-attachment-validation.utils.tsfor allowed file types.src/features/library/repositories/library.repository.tsfor library listing and delete behavior.