File Storage
Model files as part of your entity graph so uploads, downloads, and cleanup follow the same ownership and policy model as the rest of your data.
File storage is easy to make messy because most stacks split it into a second system: one set of rules for database rows, another set of rules for buckets and object paths.
The Kizaki approach is deliberately narrower and easier to reason about: a file belongs to an entity, and the entity remains the source of truth.
The Recommended Mental Model
Use a file field when uploaded content is part of a row in your app.
entity Attachment {
name: string,
file: file,
ownerId: __User.id,
@grant read, write, delete where resource.ownerId == principal.id
}That model gives you the right defaults automatically:
- upload permission follows write permission
- download permission follows read permission
- cleanup follows row replacement or row deletion
- tenant scoping follows the entity namespace
The important thing is not “Kizaki has file helpers.” The important thing is that files stop being a parallel permission system.
Start With Ownership
The cleanest first version of file storage is almost always an ownership rule:
entity UserProfile {
userId: __User.id,
avatar: file?,
@grant read to *
@grant write where resource.userId == principal.id
}That gives you a simple, legible model:
- anyone allowed to read the profile can see the avatar field
- only the profile owner can replace it
If your app’s file rules are hard to explain in one sentence, the data model is usually the first place to look, not the storage layer.
Use Separate Entities For Collections
If you need multiple files, do not try to force them into one row conceptually.
Use a separate attachment entity:
entity TaskAttachment {
taskId: Task.id @onDelete(cascade),
file: file,
uploadedBy: __User.id,
label: string?,
@grant read to * via TeamMembership
where TeamMembership.teamId == resource.task.teamId
@grant insert where resource.task.createdBy == principal.id
@grant delete where resource.uploadedBy == principal.id
}This is the right pattern for:
- galleries
- message attachments
- document versions
- task files
- reusable uploaded assets with their own labels or ordering
Once multiple files need their own identity, the answer is almost always “make them rows,” not “invent a more clever file field.”
Think In Terms Of Lifecycle
The most useful question to ask is: what should happen to the bytes when the row changes?
With Kizaki’s model, the answer stays straightforward:
- replace the field: replace the underlying file
- clear the field: remove the underlying file
- delete the row: remove the underlying file
- cascade-delete child rows: remove the child-row files too
That is much easier to trust than manual object cleanup jobs scattered across the app.
The Upload Workflow
The current runtime uses a three-step model:
- request an upload from the platform
- request an upload from the platform
- upload directly to object storage
- confirm the upload so the entity adopts the file
That matters because the entity should not point at arbitrary uploaded bytes just because a client managed to PUT something somewhere. The confirmation step is what turns an uploaded object into application state.
The Authorization Workflow
Treat file authorization exactly like entity authorization:
- can this principal write the row or field?
- can this principal read the row or field?
- should this row be public, shared, role-scoped, or owner-only?
Do not design a separate mental model for files. If you find yourself thinking about “bucket permissions” instead of entity policies, you are probably drifting away from the intended Kizaki posture.
Downloads
In normal use, the application does not serve file bytes itself. The platform issues a download URL after it has established that the principal can read the relevant entity field.
That is the right default for most apps because it preserves the clean separation:
- your app decides who can access the entity
- the platform decides how to issue the file
- object storage handles the actual transfer
Current Practical Guidance
The infrastructure path in the repo is real today: file metadata, pending-upload tracking, presigned URLs, MIME validation, and policy rechecks on confirm/download are all wired up.
What is still settling is the nicest public helper surface across every environment and template. So the safest thing to optimize for in the docs is the model:
- files belong to rows
- rows own authorization
- rows own lifecycle
- collections of files are entities
If you build around that, the helper API can improve without forcing you to rethink the architecture.
Recommended Usage
- use a
filefield when one row owns one file - use attachment entities when many files belong to one parent
- keep all access control on the entity that owns the file
- model replacement and deletion as normal row lifecycle, not storage-specific operations
- treat direct object-store details as infrastructure, not application design