QUESTPIE
Extend the PlatformCustom Adapters

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:

questpie.config.ts
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

  1. put, get, delete, exists -- basic CRUD
  2. getSignedUrl, getUrl -- URL generation for the admin UI
  3. getBytes, getStream, putStream -- binary/streaming support
  4. copy, move, deleteAll, listAll -- file management
  5. getMetaData, getVisibility, setVisibility -- metadata

Minimal example

my-storage-driver.ts
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

questpie.config.ts
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

OptionDefaultDescription
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.
signedUrlExpiration3600Token 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() returns false after delete().

Reference implementations

On this page