Signed URLs
Signed URLs grant temporary, scoped access to private files without exposing storage credentials or making the file permanently public. A signed URL embeds an HMAC-SHA256 token (signed with AUTH_SECRET) that encodes the path, operation, and expiry. Tampering with any part invalidates the token.
Signed URLs work across all three storage backends. For S3 backends, Sovrium delegates to native S3 presigning; for local and bytea backends, it generates and validates signatures internally — no AWS SDK required at the application layer.
Public vs Private Access
By default, files are private and require either authentication or a signed URL. Operators can carve out always-public path prefixes.
| Variable | Default | Purpose |
|---|---|---|
STORAGE_DEFAULT_ACCESS |
private |
Default access level for new files (private or public). |
STORAGE_PUBLIC_PATHS |
(none) | Comma-separated path prefixes served without authentication or signed URLs. |
STORAGE_DEFAULT_ACCESS=private
STORAGE_PUBLIC_PATHS=assets/,avatars/,logos/
| Behavior | Result |
|---|---|
File under a STORAGE_PUBLIC_PATHS prefix |
Served without authentication or a signed URL. |
| File outside public paths, no valid signed URL | 403 Forbidden. |
STORAGE_DEFAULT_ACCESS=public |
All files accessible without signed URLs. |
Public-path configuration is validated on startup — no trailing wildcards, no overlapping prefixes. A bucket marked public: true in the schema is the per-bucket equivalent (see Buckets Overview).
Download Signed URLs
Request a time-limited download URL for a private file.
POST /api/buckets/{bucket}/sign
Content-Type: application/json
Authorization: Bearer <token>
{
"path": "documents/contract.pdf",
"expiresIn": 3600,
"operation": "download"
}
{
"signedUrl": "https://app.example.com/api/buckets/documents/files/documents/contract.pdf?token=eyJhbGciOiJIUzI1NiJ9...",
"path": "documents/contract.pdf",
"expiresAt": "2026-04-05T11:00:00Z"
}
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
path |
string | Yes | — | File path relative to the storage root. |
expiresIn |
integer | No | 3600 |
Expiry in seconds. Range: 60 to 604800 (7 days). |
operation |
string | No | download |
download or upload. |
| Behavior | Result |
|---|---|
| Valid path | Signed URL plus expiresAt timestamp. |
| Accessing the URL before expiry | File served with no further authentication. |
| Accessing after expiry | 403 Forbidden. |
expiresIn < 60 or > 604800 |
400 Bad Request. |
| Non-existent path | 404 Not Found. |
| Tampered token | 403 Forbidden. |
Upload Signed URLs
Issue a write token so a browser can upload directly to storage without proxying bytes through the application server.
POST /api/buckets/{bucket}/sign
Content-Type: application/json
Authorization: Bearer <token>
{
"path": "uploads/user-123/profile-photo.jpg",
"expiresIn": 600,
"operation": "upload",
"contentType": "image/jpeg",
"maxSize": 5242880
}
{
"signedUrl": "https://app.example.com/api/buckets/uploads/files/uploads/user-123/profile-photo.jpg?token=eyJhbGciOiJIUzI1NiJ9...",
"path": "uploads/user-123/profile-photo.jpg",
"expiresAt": "2026-04-05T10:10:00Z",
"method": "PUT",
"headers": { "Content-Type": "image/jpeg" }
}
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
contentType |
string | No | — | Required MIME type, enforced server-side on the PUT. |
maxSize |
integer | No | 10485760 (10 MB) |
Maximum upload size in bytes. |
| Behavior | Result |
|---|---|
| Upload via the PUT URL | File stored at the specified path. |
| Upload after expiry | 403 Forbidden. |
Wrong MIME type (when contentType set) |
400 Bad Request. |
Exceeds maxSize |
413 Payload Too Large. |
| Reusing an upload token to download | Rejected — the operation is encoded in the token. |
Batch Signing
Generate signed URLs for up to 100 files in one request — useful for rendering a page full of private images.
POST /api/buckets/{bucket}/sign/batch
Content-Type: application/json
Authorization: Bearer <token>
{
"files": [
{ "path": "photos/photo-1.jpg", "expiresIn": 3600 },
{ "path": "photos/photo-2.jpg", "expiresIn": 3600 },
{ "path": "documents/report.pdf", "expiresIn": 7200 }
]
}
Each file may carry its own expiresIn. Non-existent files are returned with an error: "not_found" entry (partial success rather than failing the whole batch). Requesting more than 100 files returns 400 Bad Request.
Endpoint Summary
| Method | Endpoint | Auth |
|---|---|---|
POST |
/api/buckets/{bucket}/sign |
Required (session). |
POST |
/api/buckets/{bucket}/sign/batch |
Required (session). |
GET |
/api/buckets/{bucket}/files/{path}?token=... |
None — the token is the authentication. |
PUT |
/api/buckets/{bucket}/files/{path}?token=... |
None — the token is the authentication. |
Calling either sign endpoint without a valid session returns 401.
Access Control
Who may sign is governed by two independent layers, both using the shared PermissionValueSchema format (all | authenticated | role list).
Per-Bucket Permissions
Set sign and signUpload on the bucket.
buckets:
- name: documents
permissions:
sign: authenticated
signUpload: [admin, editor]
download: authenticated
delete: [admin]
- name: public-assets
public: true
permissions:
sign: all
signUpload: [admin]
download: all
| Behavior | Result |
|---|---|
Caller matches permissions.sign |
Can generate download signed URLs. |
Caller matches permissions.signUpload |
Can generate upload signed URLs. |
| Caller does not match | 403 Forbidden. |
Bucket has no permissions block |
Only the admin role can sign (default). |
Per-Role Storage Permissions
Roles can independently grant or revoke signing via storage.sign / storage.signUpload.
auth:
roles:
- name: member
permissions:
storage:
sign: true
signUpload: false
- name: viewer
permissions:
storage:
sign: false
signUpload: false
Admin has full signing rights by default. When no storage permissions are defined anywhere, signing defaults to admin-only.
Attachment Field Integration
When a table attachment field points at a private file, the record API response automatically embeds a signed download URL (default 1-hour expiry):
{
"id": 1,
"name": "Q1 Report",
"attachment": {
"path": "documents/q1-report.pdf",
"filename": "q1-report.pdf",
"size": 245760,
"mimeType": "application/pdf",
"signedUrl": "https://app.example.com/api/buckets/documents/files/documents/q1-report.pdf?token=...",
"signedUrlExpiresAt": "2026-04-05T11:00:00Z"
}
}
Public files include a direct URL with no token. Image attachments can carry transform parameters inside the signed URL.
Related Pages
- Buckets Overview — buckets, backends, and the
publictoggle. - File Operations — direct upload, download, and delete.
- Image Transforms — combine transforms with signed URLs.
- Attachment Fields — auto-signing in record responses.
- Environment Variables — public-path and access-level configuration.