Skip to main content
View as Markdown

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.