···11+CREATE TABLE "asset" (
22+ "id" text PRIMARY KEY NOT NULL,
33+ "workspace_id" text NOT NULL,
44+ "project_id" text NOT NULL,
55+ "task_id" text,
66+ "activity_id" text,
77+ "object_key" text NOT NULL,
88+ "filename" text NOT NULL,
99+ "mime_type" text NOT NULL,
1010+ "size" integer NOT NULL,
1111+ "kind" text DEFAULT 'image' NOT NULL,
1212+ "surface" text DEFAULT 'description' NOT NULL,
1313+ "created_by" text,
1414+ "created_at" timestamp DEFAULT now() NOT NULL,
1515+ CONSTRAINT "asset_object_key_unique" UNIQUE("object_key")
1616+);
1717+--> statement-breakpoint
1818+ALTER TABLE "asset" ADD CONSTRAINT "asset_workspace_id_workspace_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspace"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint
1919+ALTER TABLE "asset" ADD CONSTRAINT "asset_project_id_project_id_fk" FOREIGN KEY ("project_id") REFERENCES "public"."project"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint
2020+ALTER TABLE "asset" ADD CONSTRAINT "asset_task_id_task_id_fk" FOREIGN KEY ("task_id") REFERENCES "public"."task"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint
2121+ALTER TABLE "asset" ADD CONSTRAINT "asset_activity_id_activity_id_fk" FOREIGN KEY ("activity_id") REFERENCES "public"."activity"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint
2222+ALTER TABLE "asset" ADD CONSTRAINT "asset_created_by_user_id_fk" FOREIGN KEY ("created_by") REFERENCES "public"."user"("id") ON DELETE set null ON UPDATE cascade;--> statement-breakpoint
2323+CREATE INDEX "asset_workspaceId_idx" ON "asset" USING btree ("workspace_id");--> statement-breakpoint
2424+CREATE INDEX "asset_projectId_idx" ON "asset" USING btree ("project_id");--> statement-breakpoint
2525+CREATE INDEX "asset_taskId_idx" ON "asset" USING btree ("task_id");--> statement-breakpoint
2626+CREATE INDEX "asset_activityId_idx" ON "asset" USING btree ("activity_id");
···63636464## Notes on object storage
65656666-Kaneo uses S3-compatible object storage for image uploads in task descriptions and comments.
6666+Kaneo uses S3-compatible object storage for private uploads in task descriptions and comments.
67676868- Object storage is optional. Kaneo runs without it, but image uploads in task descriptions and comments will be unavailable.
6969- For local/self-hosted setups, MinIO is the recommended option when you want uploads.
7070- The Kaneo API creates presigned upload URLs and the browser uploads images directly to the configured storage backend.
7171+- Kaneo serves uploaded assets back through its own API, so the bucket does not need to be public.
7172- If you do not configure object storage, leave the `S3_*` variables unset.
7273- For a complete MinIO example and backend-specific setup notes, see the [storage backends guide](/core/installation/storage-backends).
7374
···56565757### Object Storage
58585959-Kaneo uses S3-compatible object storage for image uploads in task descriptions and task comments.
5959+Kaneo uses S3-compatible object storage for private uploads in task descriptions and task comments.
60606161MinIO is the easiest local option and the primary tested target. You can also use other S3-compatible storage backends such as AWS S3, Cloudflare R2, or [`fs`](https://github.com/ferdzo/fs).
62626363Name | Description | Default |
6464--- | --- | --- |
6565| `S3_ENDPOINT` | The S3-compatible API endpoint used by the Kaneo API for presigned uploads. Example: `http://minio:9000`. | |
6666-| `S3_BUCKET` | The bucket Kaneo will use for uploaded images. | |
6666+| `S3_BUCKET` | The bucket Kaneo will use for uploaded files. | |
6767| `S3_ACCESS_KEY_ID` | Access key used by the Kaneo API to create presigned upload URLs. | |
6868| `S3_SECRET_ACCESS_KEY` | Secret key used by the Kaneo API to create presigned upload URLs. | |
6969| `S3_REGION` | The storage region used for request signing. | `us-east-1` |
7070-| `S3_PUBLIC_BASE_URL` | Optional public base URL for uploaded assets. Use this when objects should be served from a public hostname or reverse proxy instead of the raw API endpoint. | |
7070+| `S3_PUBLIC_BASE_URL` | Optional public base URL for uploaded assets. Kaneo does not require this for the current private asset flow. | |
7171| `S3_FORCE_PATH_STYLE` | Use path-style S3 URLs. This should usually be `true` for MinIO and `fs`. | `true` |
7272-| `S3_MAX_IMAGE_UPLOAD_BYTES` | Maximum allowed image upload size in bytes. | `10485760` |
7272+| `S3_MAX_IMAGE_UPLOAD_BYTES` | Maximum allowed upload size in bytes. | `10485760` |
7373| `S3_PRESIGN_TTL_SECONDS` | How long presigned upload URLs remain valid. | `300` |
74747575### Access Control
···11---
22-title: Object storage and image uploads
33-description: Configure S3-compatible storage for task description and comment image uploads.
22+title: Object storage and uploads
33+description: Configure S3-compatible storage for private images and attachments in task descriptions and comments.
44---
5566## Overview
7788-Kaneo supports image uploads in task descriptions and task comments.
88+Kaneo supports private uploads in task descriptions and task comments.
991010Uploads use an S3-compatible object storage backend:
11111212- the Kaneo API creates presigned upload URLs
1313- the browser uploads images directly to the storage backend
1414-- Kaneo stores the final image URLs inside the task or comment content
1414+- Kaneo finalizes uploads into private asset records
1515+- Kaneo serves uploaded files back through its own API
15161617This means Kaneo can work with multiple backends as long as they expose a compatible S3-style API.
1818+1919+Current behavior:
2020+2121+- images render inline
2222+- other files such as CSV, PDF, or ZIP are inserted as attachment cards/links
2323+- uploaded files are private by default and are not meant to be served from public bucket URLs
17241825If you want backend-specific setup examples, see the [storage backends guide](/core/installation/storage-backends).
1926···44514552Create the bucket before using uploads.
46535454+You do not need to make the bucket public.
5555+4756## Important: internal Docker URLs are not enough in production
48574958When Kaneo generates a presigned upload URL, the browser uploads directly to your storage backend.
···6675S3_SECRET_ACCESS_KEY=minioadmin
6776S3_REGION=us-east-1
6877S3_FORCE_PATH_STYLE=true
6969-S3_PUBLIC_BASE_URL=https://files.cloud.kaneo.app/kaneo-uploads
7078```
71797280## Recommended public setup
···8593- Docker-internal names like `minio` are not resolvable from a user's browser
86948795Using a dedicated subdomain is recommended over proxying MinIO under a path.
9696+9797+Reads do not need a public bucket URL, because Kaneo serves uploaded assets back through `/api/asset/:id`.
88988999## Other S3-compatible backends
90100···125135| Name | Description |
126136| --- | --- |
127137| `S3_ENDPOINT` | S3-compatible API endpoint used by Kaneo |
128128-| `S3_BUCKET` | Bucket used for uploaded images |
138138+| `S3_BUCKET` | Bucket used for uploaded files |
129139| `S3_ACCESS_KEY_ID` | Access key used to create presigned upload URLs |
130140| `S3_SECRET_ACCESS_KEY` | Secret key used to create presigned upload URLs |
131141| `S3_REGION` | Region used for signing |
···135145136146| Name | Description |
137147| --- | --- |
138138-| `S3_PUBLIC_BASE_URL` | Public URL used for serving uploaded assets |
139139-| `S3_MAX_IMAGE_UPLOAD_BYTES` | Maximum allowed image size |
148148+| `S3_PUBLIC_BASE_URL` | Optional public asset base URL. Kaneo does not require this for the current private asset flow. |
149149+| `S3_MAX_IMAGE_UPLOAD_BYTES` | Maximum allowed upload size in bytes |
140150| `S3_PRESIGN_TTL_SECONDS` | Presigned upload URL lifetime |
141151142152## Testing uploads
···144154Once storage is configured:
1451551461561. Open a task.
147147-2. Paste, drag, or select an image inside the description or a comment.
148148-3. Confirm the image uploads and renders inline.
157157+2. Paste, drag, or select a file inside the description or a comment.
158158+3. Confirm images render inline and other files render as attachment cards/links.
149159150160If uploads fail:
151161···153163- confirm the credentials can write to the bucket
154164- confirm the storage endpoint is reachable from the browser
155165- confirm `S3_ENDPOINT` is a public URL, not an internal Docker hostname
156156-- confirm `S3_PUBLIC_BASE_URL` matches the public asset host if you are using one
157166- if using direct browser uploads, confirm CORS is configured correctly for your storage endpoint
+14-14
apps/docs/core/installation/storage-backends.mdx
···11---
22title: Storage backends
33-description: Configure image uploads with MinIO, AWS S3, or Cloudflare R2.
33+description: Configure private uploads with MinIO, AWS S3, or Cloudflare R2.
44---
5566## Overview
7788-Kaneo uses S3-compatible object storage for image uploads in:
88+Kaneo uses S3-compatible object storage for uploads in:
991010- task descriptions
1111- task comments
12121313The browser uploads directly to the configured storage backend using presigned URLs.
14141515+Kaneo then serves uploaded assets back through its own API.
1616+1717+Current behavior:
1818+1919+- images render inline
2020+- non-image files such as CSV, PDF, and ZIP render as attachment cards/links
2121+- assets are private by default
2222+1523That means one rule matters for every backend:
16241725- `S3_ENDPOINT` must be reachable by the browser
···2735S3_SECRET_ACCESS_KEY=
2836S3_REGION=us-east-1
2937S3_FORCE_PATH_STYLE=false
3030-S3_PUBLIC_BASE_URL=
3138```
32393340Notes:
34413535-- `S3_PUBLIC_BASE_URL` is recommended when your asset URL should be different from the raw S3 endpoint.
3642- `S3_FORCE_PATH_STYLE=true` is usually needed for MinIO.
3743- `S3_FORCE_PATH_STYLE=false` is usually correct for AWS S3 and R2.
4444+- `S3_PUBLIC_BASE_URL` is optional and not required for the current private asset flow.
38453946## MinIO
4047···5461S3_SECRET_ACCESS_KEY=minioadmin
5562S3_REGION=us-east-1
5663S3_FORCE_PATH_STYLE=true
5757-S3_PUBLIC_BASE_URL=http://localhost:9000/kaneo-uploads
5864```
59656066This works when Kaneo and MinIO are on the same Docker network and your browser reaches MinIO through `localhost`.
···7985S3_SECRET_ACCESS_KEY=minioadmin
8086S3_REGION=us-east-1
8187S3_FORCE_PATH_STYLE=true
8282-S3_PUBLIC_BASE_URL=https://files.cloud.kaneo.app/kaneo-uploads
8388```
84898590You also need:
86918792- a created bucket
8893- MinIO CORS allowing your Kaneo origin
9494+- no anonymous bucket read policy is required
89959096## AWS S3
9197···102108S3_SECRET_ACCESS_KEY=...
103109S3_REGION=us-east-1
104110S3_FORCE_PATH_STYLE=false
105105-S3_PUBLIC_BASE_URL=https://kaneo-uploads.s3.us-east-1.amazonaws.com
106111```
107112108108-For another AWS region, adjust both:
113113+For another AWS region, adjust:
109114110115- `S3_ENDPOINT`
111111-- `S3_PUBLIC_BASE_URL`
112116113117Recommended S3 CORS policy:
114118···138142S3_SECRET_ACCESS_KEY=...
139143S3_REGION=auto
140144S3_FORCE_PATH_STYLE=false
141141-S3_PUBLIC_BASE_URL=https://pub-<hash>.r2.dev
142145```
143146144147Notes:
145148146149- `S3_REGION=auto` is typical for R2
147147-- `S3_PUBLIC_BASE_URL` should usually point to your public bucket domain or custom domain
148148-- if your bucket is not public, image rendering will not work from stored asset URLs
150150+- a public bucket is not required for Kaneo's current private asset flow
149151150152## One copy-paste self-hosted example
151153···212214S3_SECRET_ACCESS_KEY=change-me
213215S3_REGION=us-east-1
214216S3_FORCE_PATH_STYLE=true
215215-S3_PUBLIC_BASE_URL=https://files.cloud.kaneo.app/kaneo-uploads
216217```
217218218219Before testing uploads:
···227228228229- check that the bucket exists
229230- check that `S3_ENDPOINT` is public and browser-reachable
230230-- check that `S3_PUBLIC_BASE_URL` points to the correct public asset host
231231- check CORS on your storage backend
232232- check that the access key can `PutObject` and `GetObject`