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
| Option | Type | Description |
|---|---|---|
bucket | string | S3 bucket name |
region | string | AWS region (e.g., us-east-1) |
accessKeyId | string | AWS access key |
secretAccessKey | string | AWS secret key |
endpoint | string | Custom endpoint for S3-compatible APIs |
forcePathStyle | boolean | Use path-style URL generation |
signedUrlExpiry | number | Redirect 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
allowedOriginsconfig - For private S3 files, use
access.read: trueand omits3.signedUrlExpiryso 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
},
},
});
| Option | Default | Description |
|---|---|---|
access.write | false | Require authentication to upload files |
access.read | false | Require 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}` },
});