Storage

Lucent provides file storage with support for local filesystem, S3-compatible storage, and Google Drive.

Configuration

Local Storage

export default defineConfig({
  storage: {
    provider: "local",
    dir: "./uploads",
    prefix: "/uploads",
    maxSize: 10 * 1024 * 1024, // 10MB
    allowedTypes: ["image/*", "application/pdf"],
  },
});

S3 Storage

export default defineConfig({
  upload: {
    provider: "s3",
    prefix: "/uploads",
    dir: "uploads",
    s3: {
      bucket: process.env.S3_BUCKET,
      region: process.env.S3_REGION,
      accessKeyId: process.env.AWS_ACCESS_KEY_ID,
      secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
      // Optional: custom endpoint for S3-compatible services
      endpoint: "https://storage.example.com",
      // Optional: force path style
      forcePathStyle: true,
    },
  },
});

prefix is the Lucent URL path used to serve files. dir is the object key prefix inside the S3 bucket. For S3, prefer dir: "uploads" instead of dir: "/uploads" so object keys are stored without a leading slash.

Google Drive Storage

Google Drive storage uses a service account and is always scoped to one configured folder. The folder must be in a Google Workspace Shared drive, or the service account must use domain-wide delegation to act as a Workspace user with Drive storage quota. A normal My Drive folder shared with the service account is not enough for uploads because service accounts do not have personal Drive storage quota.

Share the Shared drive folder with the service account email, then configure Lucent with the folder ID or folder URL.

export default defineConfig({
  upload: {
    provider: "googleDrive",
    prefix: "/uploads",
    googleDrive: {
      clientEmail: process.env.GOOGLE_DRIVE_CLIENT_EMAIL,
      privateKey: process.env.GOOGLE_DRIVE_PRIVATE_KEY,
      folderId: process.env.GOOGLE_DRIVE_FOLDER_ID,
    },
  },
});

folderId is required. Lucent accepts either the raw folder ID or a Google Drive folder URL. Lucent uploads, reads, deletes, and registry entries are kept inside that folder boundary.

Environment:

GOOGLE_DRIVE_CLIENT_EMAIL=lucent-storage@example.iam.gserviceaccount.com
GOOGLE_DRIVE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n"
GOOGLE_DRIVE_FOLDER_ID=1AbCdEfGhIjKlMnOpQrStUvWxYz

File Fields

Define file fields in your collections:

export const posts = defineCollection({
  slug: "posts",
  fields: [
    { name: "title", type: "text", required: true },
    // Single file upload
    { name: "coverImage", type: "upload" },
    // Multiple files
    { name: "attachments", type: "upload", multiple: true },
    // Specific file types
    { name: "avatar", type: "upload", allowedTypes: ["image/*"] },
    // With size limit (in bytes)
    { name: "document", type: "upload", maxSize: 10 * 1024 * 1024 },
  ],
});

Using Storage

import { createStorage } from "@codesordinatestudio/lucent";

// Create storage instance
const storage = createStorage({
  provider: "local",
  dir: "./uploads",
});

// Upload a file
const file = await storage.saveFile(fileInput, {
  allowedTypes: ["image/*"],
  maxSize: 5 * 1024 * 1024,
});

// Get file URL
const url = storage.getUrl(file.path);

// Delete a file
await storage.delete(file.path);

Storage API

Every successful upload is also registered in the internal lucent_uploads table. This gives clients a provider-agnostic file library for picker UIs.

GET /api/uploads?q=invoice&mime=application/pdf&provider=googleDrive

Returns paginated upload records from lucent_uploads.

saveFile(file, options?)

Saves a file and returns metadata:

const uploaded = await storage.saveFile(file, {
  allowedTypes: ["image/jpeg", "image/png"],
  maxSize: 5 * 1024 * 1024,
});
// Returns: { path, filename, originalName, mimeType, size, url }

getUrl(path)

Generates a public URL for a stored file:

const url = storage.getUrl("abc123.jpg");
// Local: "/uploads/abc123.jpg"
// S3: "https://bucket.s3.region.amazonaws.com/abc123.jpg"

delete(path)

Removes a file from storage:

await storage.delete("abc123.jpg");

exists(path)

Checks if a file exists:

const exists = await storage.exists("abc123.jpg");

copy(src, dest)

Copies a file:

await storage.copy("original.jpg", "backup.jpg");

move(src, dest)

Moves/renames a file:

await storage.move("temp.jpg", "permanent.jpg");

list(prefix?)

Lists files in a directory:

const files = await storage.list("avatars/");

S3 Configuration Options

OptionTypeDescription
bucketstringS3 bucket name
regionstringAWS region (e.g., us-east-1)
accessKeyIdstringAWS access key
secretAccessKeystringAWS secret key
endpointstringCustom endpoint for S3-compatible APIs
forcePathStylebooleanUse path-style URL generation
signedUrlExpirynumberRedirect downloads to a presigned S3 URL valid for this many seconds

Private S3 Files Through Lucent

For private documents, keep the bucket private and make Lucent the only gateway. Lucent has the S3 credentials, so the browser should request the Lucent upload URL, Lucent checks authentication, then Lucent fetches the object from S3 and streams it back to the user.

Recommended private S3 config:

export default defineConfig({
  upload: {
    provider: "s3",
    prefix: "/uploads",
    dir: "uploads",
    access: {
      write: true,
      read: true,
    },
    s3: {
      bucket: process.env.S3_BUCKET,
      region: process.env.S3_REGION,
      accessKeyId: process.env.AWS_ACCESS_KEY_ID,
      secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
      endpoint: process.env.S3_ENDPOINT,
      forcePathStyle: true,

      // Do not set signedUrlExpiry for strict private-through-Lucent access.
    },
  },
});

With this setup, stored upload URLs point at Lucent, for example:

https://api.example.com/api/uploads/uploads%2F019e7f59-31b1-7000-bf6a-81be456bf1b1.png

Do not expose S3 URLs for files that must require a Lucent access token.

Presigned (expiring) download URLs

When signedUrlExpiry is set on the S3 config, the GET /uploads/:filename route redirects (HTTP 302) to a time-limited presigned S3 URL instead of proxying the file through your server. This removes bandwidth load from your server and lets S3 enforce the expiry automatically.

This is not the strict private-document mode. A presigned URL is temporary, but it is still bearer access: anyone who has the full URL can read the file until it expires. Use it only when temporary shareable access is acceptable. If every read must require Lucent authentication, do not set signedUrlExpiry.

export default defineConfig({
  upload: {
    provider: "s3",
    s3: {
      bucket: process.env.S3_BUCKET,
      region: process.env.S3_REGION,
      accessKeyId: process.env.AWS_ACCESS_KEY_ID,
      secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
      // Downloads redirect to a signed URL that expires after 1 hour
      signedUrlExpiry: 3600,
    },
  },
});

Security

  • Filenames are UUID-generated to prevent conflicts and path traversal
  • File type validation based on MIME type
  • Size limits enforced on upload
  • Cross-origin upload protection via allowedOrigins config
  • For private S3 files, use access.read: true and omit s3.signedUrlExpiry so every read is authenticated by Lucent before the object is fetched from S3
  • Presigned S3 URLs are temporary but shareable; anyone with the URL can access the file until expiry
  • Extension denylist — executable and script extensions (.html, .js, .php, .sh, .exe, .py, .rb, and many others) are always rejected regardless of the reported MIME type
  • Content inspection — the first bytes of every uploaded file are checked for HTML/script signatures; files that look like HTML but claim to be an image or other safe type are rejected

Access Control

By default, upload and download endpoints are public. You can restrict them to authenticated users via the access config in upload:

// lucent.config.ts
export default defineConfig({
  upload: {
    provider: "local",
    dir: "./uploads",
    access: {
      write: true, // POST /api/upload requires a logged-in user
      read: true, // GET /uploads/:filename requires a logged-in user
    },
  },
});
OptionDefaultDescription
access.writefalseRequire authentication to upload files
access.readfalseRequire authentication to download/view files

When access is denied the server responds with 401 Unauthorized. The check uses the same authentication middleware (JWT / API key) as the collection endpoints, so any valid session token is accepted.

Protected uploads example

// Upload — include your auth token
const form = new FormData();
form.append("file", fileInput.files[0]);

const res = await fetch("/api/upload", {
  method: "POST",
  headers: { Authorization: `Bearer ${token}` },
  body: form,
});

const { url } = await res.json();
// url = "/uploads/4f3a2b1c-....jpg"

// Download — include your auth token
const img = await fetch(url, {
  headers: { Authorization: `Bearer ${token}` },
});