Storage Adapter
Implement a custom file storage driver for QUESTPIE using FlyDrive.
Storage in QUESTPIE is backed by FlyDrive. To use a custom cloud provider (S3, R2, GCS, Azure Blob, etc.), you provide a FlyDrive DriverContract instance. FlyDrive already ships drivers for popular providers -- you only need to write a custom driver if your provider is not supported.
Interface
QUESTPIE expects a FlyDrive DriverContract. The full interface:
interface DriverContract {
// Core operations
put(key: string, contents: string | Uint8Array, options?: WriteOptions): Promise<void>;
get(key: string): Promise<string>;
getBytes(key: string): Promise<Uint8Array>;
getStream(key: string): Promise<ReadableStream<Uint8Array>>;
delete(key: string): Promise<void>;
exists(key: string): Promise<boolean>;
// URLs
getUrl(key: string): Promise<string>;
getSignedUrl(key: string, options?: SignedURLOptions): Promise<string>;
// Metadata & visibility
getMetaData(key: string): Promise<ObjectMetaData>;
getVisibility(key: string): Promise<ObjectVisibility>;
setVisibility(key: string, visibility: ObjectVisibility): Promise<void>;
// Streaming upload
putStream(key: string, contents: Readable, options?: WriteOptions): Promise<void>;
// File operations
copy(source: string, destination: string, options?: WriteOptions): Promise<void>;
move(source: string, destination: string, options?: WriteOptions): Promise<void>;
// Bulk operations
deleteAll(prefix: string): Promise<void>;
listAll(prefix: string, options?: { recursive?: boolean; paginationToken?: string }): Promise<{
paginationToken?: string;
objects: Iterable<DriveFile | DriveDirectory>;
}>;
}Using an existing FlyDrive driver
For most cloud providers, you do not need to write a driver at all. Install the FlyDrive package for your provider and pass it directly:
import { config } from "questpie";
import { S3Driver } from "flydrive/drivers/s3";
export default config({
// ...
storage: {
driver: new S3Driver({
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
},
region: process.env.AWS_REGION!,
bucket: process.env.S3_BUCKET!,
}),
},
});FlyDrive ships drivers for S3, R2 (Cloudflare), GCS (Google Cloud Storage), and local filesystem. See the FlyDrive drivers documentation for the full list.
Writing a custom driver
If your provider is not covered by FlyDrive, implement DriverContract. Start with the core four methods, then fill in the rest.
Priority order
put,get,delete,exists-- basic CRUDgetSignedUrl,getUrl-- URL generation for the admin UIgetBytes,getStream,putStream-- binary/streaming supportcopy,move,deleteAll,listAll-- file managementgetMetaData,getVisibility,setVisibility-- metadata
Minimal example
import type {
DriverContract,
ObjectMetaData,
ObjectVisibility,
SignedURLOptions,
WriteOptions,
} from "flydrive/types";
export class MyCloudDriver implements DriverContract {
private client: any; // Your provider SDK client
constructor(private options: { apiKey: string; bucket: string }) {
// Initialize your provider SDK
}
async put(key: string, contents: string | Uint8Array, _options?: WriteOptions): Promise<void> {
const body = typeof contents === "string"
? new TextEncoder().encode(contents)
: contents;
await this.client.upload(this.options.bucket, key, body);
}
async get(key: string): Promise<string> {
const bytes = await this.client.download(this.options.bucket, key);
return new TextDecoder().decode(bytes);
}
async getBytes(key: string): Promise<Uint8Array> {
return this.client.download(this.options.bucket, key);
}
async getStream(key: string): Promise<ReadableStream<Uint8Array>> {
const bytes = await this.getBytes(key);
return new ReadableStream({
start: (controller) => {
controller.enqueue(bytes);
controller.close();
},
});
}
async delete(key: string): Promise<void> {
await this.client.remove(this.options.bucket, key);
}
async exists(key: string): Promise<boolean> {
try {
await this.client.head(this.options.bucket, key);
return true;
} catch {
return false;
}
}
async getUrl(key: string): Promise<string> {
return `https://${this.options.bucket}.example.com/${key}`;
}
async getSignedUrl(key: string, _options?: SignedURLOptions): Promise<string> {
return this.client.presign(this.options.bucket, key);
}
async getMetaData(_key: string): Promise<ObjectMetaData> {
return {};
}
async getVisibility(_key: string): Promise<ObjectVisibility> {
return "public";
}
async setVisibility(_key: string, _visibility: ObjectVisibility): Promise<void> {
// Map to your provider's ACL system
}
async putStream(_key: string, _contents: any, _options?: WriteOptions): Promise<void> {
throw new Error("Implement stream uploads for production use");
}
async copy(source: string, destination: string, _options?: WriteOptions): Promise<void> {
const data = await this.getBytes(source);
await this.put(destination, data);
}
async move(source: string, destination: string, options?: WriteOptions): Promise<void> {
await this.copy(source, destination, options);
await this.delete(source);
}
async deleteAll(prefix: string): Promise<void> {
const list = await this.listAll(prefix);
for (const obj of list.objects) {
if ("key" in obj) await this.delete(obj.key);
}
}
async listAll(_prefix: string): Promise<{ objects: Iterable<any> }> {
return { objects: [] };
}
}Registration
import { config } from "questpie";
import { MyCloudDriver } from "./my-storage-driver";
export default config({
// ...
storage: {
driver: new MyCloudDriver({
apiKey: process.env.STORAGE_API_KEY!,
bucket: process.env.STORAGE_BUCKET!,
}),
defaultVisibility: "public", // "public" | "private" (default: "public")
signedUrlExpiration: 3600, // seconds (default: 3600)
basePath: "/", // base path for serving (default: "/")
},
});Config options
| Option | Default | Description |
|---|---|---|
driver | -- | FlyDrive DriverContract instance. If omitted, QUESTPIE uses local FSDriver. |
location | "./uploads" | Directory for local storage (only when driver is not set). |
defaultVisibility | "public" | Default visibility for uploaded files. |
signedUrlExpiration | 3600 | Token expiration for signed URLs (seconds). |
basePath | "/" | Base path for serving storage files. |
You must provide either driver (custom/cloud) or location (local), not both.
Testing tips
- Upload one file, read it back, then delete it -- basic round-trip.
- Test both public URLs (
getUrl) and signed URLs (getSignedUrl). - Do not normalize keys inside the driver -- FlyDrive expects keys to be stored exactly as received.
- If your backend supports pagination, test
listAll()pagination tokens explicitly. - Test
exists()returnsfalseafterdelete().
Reference implementations
- QUESTPIE storage driver factory -- shows how QUESTPIE creates the default FSDriver with signed URLs
- FlyDrive custom driver guide -- official FlyDrive documentation for writing drivers
- FlyDrive S3 driver source -- reference for a production driver