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
})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
import { smtpAdapter, consoleAdapter } from "questpie/email";
// SMTP (production)
email: {
adapter: smtpAdapter({
host: "smtp.example.com",
port: 587,
auth: { user: "...", pass: "..." },
from: "noreply@example.com",
}),
}
// Console (development — prints to stdout)
email: {
adapter: consoleAdapter(),
}Defining Email Templates
import { email } from "questpie";
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
import { pgBossAdapter } from "questpie/queue-pg-boss";
queue: {
adapter: pgBossAdapter({
connectionString: process.env.DATABASE_URL!,
}),
}Defining Jobs
import { job } from "questpie";
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,
},
});Search
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
// Local filesystem
storage: {
driver: "local",
basePath: "./uploads",
}
// S3 / S3-compatible
storage: {
driver: "s3",
s3: {
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), { ttl: 3600 });
// Get (returns string | null)
const value = await kv.get("cache:settings");
// Delete
await kv.delete("cache:settings");Configuration
// Redis (production)
kv: {
adapter: "redis",
url: process.env.REDIS_URL!,
}
// In-memory (development)
kv: {
adapter: "memory",
}Realtime
Event broadcasting for live updates.
Broadcasting
await realtime.broadcast("posts", {
type: "post.updated",
data: { id: post.id, title: post.title },
});Configuration
import { redisStreamsAdapter } from "questpie/realtime-redis";
import { pgNotifyAdapter } from "questpie/realtime-pg";
// Redis Streams (production, multi-instance)
realtime: {
adapter: redisStreamsAdapter({ url: process.env.REDIS_URL! }),
}
// 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);