QUESTPIE

Infrastructure API

Service APIs — email, queue, storage, KV, realtime, logger, and database.

All infrastructure services are available through handler context in routes, hooks, and jobs.

.handler(async ({ db, email, queue, kv, logger, app }) => {
  // All services available here
})

Email

Send emails using registered templates or raw HTML.

Sending a Template

await email.sendTemplate({
	template: "welcome",
	input: { name: "Alice", verifyUrl: "https://..." },
	to: "alice@example.com",
});

Adapter Configuration

questpie.config.ts
import { ConsoleAdapter } from "questpie/adapters/console";
import { PlunkAdapter } from "questpie/adapters/plunk";
import { ResendAdapter } from "questpie/adapters/resend";
import { SmtpAdapter } from "questpie/adapters/smtp";

// Resend or a Resend-compatible API
email: {
  adapter: new ResendAdapter({
    apiKey: process.env.RESEND_API_KEY!,
  }),
}

// Plunk
email: {
  adapter: new PlunkAdapter({
    apiKey: process.env.PLUNK_SECRET_KEY!,
  }),
}

// SMTP (production)
email: {
  adapter: new SmtpAdapter({
    transport: {
      host: "smtp.example.com",
      port: 587,
      auth: { user: "...", pass: "..." },
    },
  }),
}

// Console (development — prints to stdout)
email: {
  adapter: new ConsoleAdapter(),
}

Defining Email Templates

emails/welcome.ts
import { email } from "questpie/services";
import { z } from "zod";

export default email({
	name: "welcome",
	schema: z.object({ name: z.string(), verifyUrl: z.string() }),
	handler: ({ input }) => ({
		subject: `Welcome, ${input.name}!`,
		html: `<h1>Welcome!</h1><a href="${input.verifyUrl}">Verify</a>`,
	}),
});

Queue

Background job processing backed by pg-boss (PostgreSQL).

Publishing a Job

await queue.sendWelcomeEmail.publish({
	userId: user.id,
	email: user.email,
});

Jobs are typed -- the payload must match the job's Zod schema.

Adapter Configuration

questpie.config.ts
import { pgBossAdapter } from "questpie/adapters/pg-boss";

queue: {
  adapter: pgBossAdapter({
    connectionString: process.env.DATABASE_URL!,
  }),
}

Defining Jobs

jobs/send-welcome-email.ts
import { job } from "questpie/services";
import { z } from "zod";

export default job({
	name: "sendWelcomeEmail",
	schema: z.object({
		userId: z.string(),
		email: z.string().email(),
	}),
	handler: async ({ payload, email }) => {
		await email.sendTemplate({
			template: "welcome",
			input: { name: payload.userId },
			to: payload.email,
		});
	},
	options: {
		retryLimit: 3,
		retryDelay: 5000,
	},
});

Full-text search using PostgreSQL FTS with BM25 ranking.

Configuring Search on a Collection

collection("posts")
  .fields(({ f }) => ({ ... }))
  .searchable({
    content: (record) => `${record.title} ${extractText(record.content)}`,
    metadata: (record) => ({ status: record.status }),
  })

The content callback extracts plain text for full-text indexing. The metadata callback provides structured data for faceted filtering.

Searching

const results = await collections.posts.find({
	search: "typescript tutorial",
	where: { status: "published" },
});

Storage

File storage using FlyDrive with local or S3 backends.

Configuration

questpie.config.ts
import { S3Driver } from "flydrive/drivers/s3";

// Local filesystem
storage: {
  location: "./uploads",
  basePath: "/api",
}

// S3 / S3-compatible
storage: {
  driver: new S3Driver({
    bucket: "my-bucket",
    region: "us-east-1",
    accessKeyId: "...",
    secretAccessKey: "...",
    endpoint: "https://...",  // For S3-compatible providers
  }),
}

Upload Collections

Collections with .upload() automatically handle file storage:

collection("media")
	.fields(({ f }) => ({
		alt: f.text(),
		folder: f.text(),
	}))
	.upload({
		visibility: "public",
		maxSize: 10_000_000,
		allowedTypes: ["image/*", "application/pdf"],
	});

Upload collections automatically:

  • Add storage fields (key, filename, mimeType, size, visibility)
  • Extend the output type with { url: string }
  • Generate signed URLs for private files
  • Register HTTP upload and file-serving routes

Using Storage Directly

// Upload a file
const disk = app.storage.use();
await disk.put("path/to/file.txt", content);

// Get URL
const url = await disk.getUrl("path/to/file.txt");

// Get signed URL (private files)
const signedUrl = await disk.getSignedUrl("path/to/file.txt");

KV Store

Key-value store for caching and ephemeral data.

API

// Set with optional TTL (seconds)
await kv.set("cache:settings", JSON.stringify(settings), 3600);

// Get (returns string | null)
const value = await kv.get("cache:settings");

// Delete
await kv.delete("cache:settings");

Configuration

questpie.config.ts
import { createClient } from "redis";
import { redisKVAdapter } from "questpie/adapters/redis-kv";

async function getRedis() {
	const redis = createClient({ url: process.env.REDIS_URL! });
	await redis.connect();
	return redis;
}

// Redis
kv: {
  adapter: redisKVAdapter({ client: getRedis, keyPrefix: "my-app:" }),
  defaultTtl: 3600,
}

// Custom adapter
kv: {
  adapter: myKvAdapter,
  defaultTtl: 3600,
}

// In-memory default
kv: {
  defaultTtl: 3600,
}

Realtime

Event broadcasting for live updates.

Broadcasting

await realtime.broadcast("posts", {
	type: "post.updated",
	data: { id: post.id, title: post.title },
});

Configuration

questpie.config.ts
import { pgNotifyAdapter } from "questpie/adapters/pg-notify";
import { redisStreamsAdapter } from "questpie/adapters/redis-streams";

// Redis Streams (production, multi-instance)
realtime: {
  adapter: redisStreamsAdapter({ client: redis }),
}

// PostgreSQL NOTIFY (simpler, single-instance)
realtime: {
  adapter: pgNotifyAdapter({ connectionString: process.env.DATABASE_URL! }),
}

Logger

Structured logging via Pino.

logger.info("User created");
logger.info({ userId: user.id }, "User created");
logger.warn({ attempt: 3 }, "Rate limit approaching");
logger.error({ err: error }, "Failed to process webhook");
logger.debug({ payload }, "Incoming request");

Log levels: trace, debug, info, warn, error, fatal.


Database

Direct access to the Drizzle ORM instance for custom queries.

import { eq, sql } from "drizzle-orm";

// Raw SQL
const result = await db.execute(
	sql`SELECT COUNT(*) FROM posts WHERE status = 'published'`,
);

// Drizzle query builder
const rows = await db
	.select()
	.from(postsTable)
	.where(eq(postsTable.status, "published"))
	.orderBy(postsTable.createdAt)
	.limit(10);

App Instance

The app instance provides access to the full runtime for scripts and advanced use cases.

// Create a system context (for scripts, cron jobs)
const ctx = await app.createContext({ accessMode: "system", locale: "en" });

// Use the collection API directly
const posts = await app.collections.posts.find({}, ctx);
const settings = await app.globals.siteSettings.get(ctx);

On this page