MODULES / CONTENT
Files
Prov0.1.0A file manager for your users on Supabase Storage: folders, direct-to-storage uploads with quotas, previews, sharing with other users and expiring public links, plus an admin console for storage usage.
terminalnpx ajolla add filesWhat you get
- Browser at /files: nested folders, breadcrumbs, grid or list view, search and multi-select
- Drag-and-drop uploads straight to Supabase Storage with progress bars (no server body limits)
- Per-file size limit, allowed file types and per-user quotas with a usage meter
- Image thumbnails and image, PDF, text, audio and video previews
- Share files and folders with other users as viewers or editors, enforced by RLS
- Public links with optional expiry, served as short-lived signed URLs; revoke any time
- Trash with restore, permanent delete and automatic purge after a retention period
- Admin console: total and per-user usage, quota overrides, largest files and removal
Routes it adds
/admin/files/files/files/[fileId]/files/folders/[folderId]/sharing/files/shared/files/trash
/api/ajolla/files/cleanup/api/ajolla/files/download/[fileId]/api/ajolla/files/link/[token]
Plus 1 Supabase migration with row-level security, and an AGENTS.md your coding agent reads after install.
Installs with
Kernel (added automatically).
Configuration
- FILES_LINK_SECRET
- Random secret (32+ characters) that signs public link tokens. Falls back to one derived from SUPABASE_SECRET_KEY; rotating it invalidates every public link.
Events
Other modules (and your own code) can react to these:
files.uploadedfiles.deletedfiles.sharednotifications.requested
AGENTS.md
Read the instructions installed with this module
markdown# Ajolla Files
A file manager for your signed-in users on **your own Supabase Storage**: nested folders, direct-to-storage uploads with quotas, previews, sharing with other users, expiring public links, a trash, and an admin console. "Admins" means users with the `files.manage` permission.
## What was installed
**Routes**
- `/files`: the browser. It shows breadcrumbs, a grid or list view (`?view=list`), search by name (`?q=`), and a usage meter.
- Select several items to rename, move or delete them.
- Upload by drag-and-drop or the button, with progress bars and cancel.
- Open a folder with `?folder=<id>`. This also works for folders shared with you.
- `/files/[fileId]`: image, PDF, text, audio or video preview, with details, download, rename and trash. Owners also manage people and public links here.
- `/files/folders/[folderId]/sharing`: share a folder with people.
- `/files/shared`: what others shared with you. **Leave** removes your access.
- `/files/trash`: restore, delete forever, or empty the trash.
- `/admin/files`: totals, upload settings, per-user usage with quota overrides, and the largest files with removal.
- `GET /api/ajolla/files/download/[fileId]`: a signed-in download. It redirects to a 60-second signed Storage URL.
- `GET /api/ajolla/files/link/[token]`: a public link. It checks the token's HMAC, that the link exists and hasn't expired, then redirects to a 60-second signed URL.
- `GET /api/ajolla/files/cleanup`: the cron (Bearer `CRON_SECRET`). It removes abandoned uploads and purges trash past the retention period.
**Code**
- `lib/ajolla/files/core.ts`: all logic, framework-free. Storage goes through the `FileStorage` interface.
- `lib/ajolla/files/shared.ts`: types, limits and pure helpers, safe in the browser.
- `lib/ajolla/files/server.ts`: the Supabase Storage adapter, link-token signing, and user lookup by email.
- `components/ajolla/files/`: the browser, the uploader hook, the sharing panels, previews and the CSS module.
- `lib/ajolla/admin/files.ts`: the admin page registration.
**Storage**
- Private bucket `ajolla-files`, with a 50 MB per-object cap (`file_size_limit`).
- Objects live at `<owner user id>/<file id>`.
**Tables** (`ajolla_files_`)
- `folders`: nested. Live sibling names are unique, case-insensitively.
- `files`: metadata. `status` is `pending` until the server verifies the upload. Trash fields: `trashed_at`, `trashed_by`, `trash_batch`, `trash_root`.
- `shares`: one row per file or folder per recipient, with `read` or `write`.
- `links`: public links, with optional `expires_at` and a download count.
- `usage`: bytes and files per user, kept by a trigger.
- `quotas`: per-user overrides.
- `settings`: one row of admin settings. When absent, these defaults apply: 25 MB per file, 1 GB quota, any type, links on, 30-day trash.
**Access and RLS**
- The SECURITY DEFINER helpers `ajolla_files_folder_access`, `ajolla_files_file_access`, `ajolla_files_can_read(path)`, `ajolla_files_can_upload(path)`, `ajolla_files_can_manage(path)` and `ajolla_files_upload_allowed` compute access. They have `search_path ''`.
- **Owner:** owning an item, or any folder above it, makes you its owner.
- **Shares:** a share on an item or any folder above it grants `read` (view and download) or `write` (also upload, create folders, rename and move inside).
- **Trash:** only owners can trash, restore or delete. Trashed items disappear for collaborators.
- **Inserts:** RLS checks the size limit, allowed types and quota on every file insert. Users can only insert `pending` rows.
- **Guard triggers:**
- Users can't change owner, size, path, type or status.
- Users can't move items somewhere they can't write.
- Folders can't be moved into their own descendants.
- **storage.objects:**
- Read: `ajolla_files_can_read(name)`.
- Upload: only onto your own pending row.
- Delete: your own folder, items you own through a folder, or admins.
- **Admins** (`files.manage`) read everything, remove any file, and edit settings and quotas.
- **Anonymous visitors** get nothing.
**Upload flow** (file bodies never touch your server functions, so Vercel's 4.5 MB body limit doesn't apply)
1. `prepareUploadAction` checks limits and quota, inserts `pending` rows (which reserve quota), and returns a signed upload URL per file.
2. The browser PUTs each file to Storage with progress, three at a time.
3. `completeUploadAction` reads the stored object's real size. If it is missing or larger than reserved, the object and row are deleted. Otherwise the row is marked `ready`. This step uses the secret key on purpose: users can never set `ready` themselves.
**Events**
- `files.uploaded`: `{ userId, fileIds, folderId, count, bytes }`.
- `files.deleted`: `{ userId?, fileIds, folders, bytes, by: "owner" | "admin" | "retention" }`, on permanent deletion only.
- `files.shared`: `{ userId, sharedWith, shareId, fileId, folderId, permission, via: "user" }`, or `{ userId, fileId, linkId, expiresAt, via: "link" }`.
- `notifications.requested` goes to the recipient when something is shared with them. Install Notifications to deliver it.
**Env**
- `FILES_LINK_SECRET` (optional): signs public-link tokens. It defaults to a value derived from `SUPABASE_SECRET_KEY`. Rotating it breaks every existing link.
- `NEXT_PUBLIC_SITE_URL` (optional): makes copied links use your public URL instead of the request host.
- `CRON_SECRET` (from the kernel): protects the clean-up cron.
## Setup
1. Apply the migration with `supabase db push`. It creates the private `ajolla-files` bucket and its storage policies.
2. Add a cron for the clean-up route, e.g. in `vercel.json`:
```json
{ "crons": [{ "path": "/api/ajolla/files/cleanup", "schedule": "17 3 * * *" }] }
```
3. Link to `/files` from your navigation, and set the limits at `/admin/files`.
4. **Files over 50 MB:** raise both the bucket limit and Supabase's global upload limit (Project settings → Storage), then the per-file limit in `/admin/files`:
```sql
update storage.buckets set file_size_limit = 524288000 where id = 'ajolla-files';
```
Uploads use a single PUT. Supabase recommends resumable (TUS) uploads above about 6 MB on flaky networks. You can swap the `put()` helper in `components/ajolla/files/uploader.ts` for `tus-js-client` if you need that.
## Limits and costs
Storage and egress are billed by your Supabase plan, which is why this module isn't offered on Ajolla Hosted.
**Built-in caps**
- 50 files per upload batch.
- The per-file limit and per-user quota.
- 20 public links per file.
- Signed URLs last 60 s for downloads and 10 min for previews and thumbnails.
- Thumbnails only for images up to 10 MB.
- Text previews read only the first 200 KB.
**Quota accounting**
- Pending uploads and trashed files count toward quota until they are purged.
- The quota check can race when one user uploads in two tabs at once; the overshoot is at most one batch.
**Public links**
- Public link downloads count toward your egress.
- Turn links off in `/admin/files`, or put Vercel Firewall rate limiting on `/api/ajolla/files/link/*`.
**Account deletion**
- Deleting a user cascades their rows, but Storage objects aren't removed by the database.
- Remove `<user id>/` from the bucket in your account-deletion flow.
## Customising
- **Defaults and caps:** `DEFAULT_SETTINGS`, `MAX_UPLOAD_BATCH`, `PENDING_UPLOAD_TTL_MS`, `THUMBNAIL_MAX_BYTES` and the TTLs in `shared.ts`. If you change the defaults, keep them in sync with the `ajolla_files_upload_allowed` fallbacks in the migration.
- **Previewable types:** `fileKind()` and `canPreview()` in `shared.ts`.
- **Styles:** `components/ajolla/files/files.module.css`, plus the kit's `--aj-*` variables.