@cfast/core
Overview
Section titled “Overview”@cfast/core is the optional composition layer for the cfast framework. It provides createApp() — a single definition that connects @cfast/env, @cfast/permissions, @cfast/auth, @cfast/db, @cfast/storage, and any other plugins into a typed per-request context on the server and a unified provider tree on the client.
Without core, every route loader repeats the same initialization: parse env, get the auth context, create a db client. Core eliminates this by running a plugin chain once per request and composing client providers automatically. Individual packages remain fully usable on their own.
Installation
Section titled “Installation”pnpm add @cfast/corePeer dependencies: @cfast/env, @cfast/permissions, and react (for client-side providers).
Quick Setup
Section titled “Quick Setup”Define your app in a single file:
import { createApp } from "@cfast/core";import { authPlugin } from "@cfast/auth";import { dbPlugin } from "@cfast/db";import { storagePlugin } from "@cfast/storage";import { envSchema } from "./env";import { permissions } from "./permissions";
export const app = createApp({ env: envSchema, permissions }) .use(authPlugin({ magicLink: { sendMagicLink: async ({ email, url }) => { /* ... */ } }, session: { expiresIn: "30d" }, defaultRoles: ["reader"], })) .use(dbPlugin({ schema })) .use(storagePlugin(storageSchema));createApp takes the two leaf packages (env and permissions) as direct config because every cfast app needs them. Everything else is a plugin registered via .use().
Initialize in the Workers entry point:
import { app } from "~/cfast";import { requestHandler } from "react-router";
export default { async fetch(request: Request, rawEnv: Record<string, unknown>, ctx: ExecutionContext) { app.init(rawEnv); return requestHandler(request, { cloudflare: { env: app.env(), ctx }, }); },};Core Concepts
Section titled “Core Concepts”Plugin Architecture
Section titled “Plugin Architecture”Plugins run in registration order, not dependency-sorted. You read the .use() chain top to bottom and know exactly what runs when. Each plugin’s setup() receives everything prior plugins have provided, plus request and env:
authPlugin.setup({ request, env })returns{ user, grants, instance }dbPlugin.setup({ request, env, auth: { ... } })returns{ client }storagePlugin.setup({ request, env, auth: { ... }, db: { ... } })returns{ handle, ... }
Each plugin’s return value is nested under its name key in the context (e.g., ctx.auth.user, not ctx.user). This prevents plugins from silently overriding each other’s values. Core throws at startup if two plugins share a name.
Per-Request Context
Section titled “Per-Request Context”app.context(request, context) builds the full context by running each plugin’s setup() in order. The return type is the intersection of all plugin namespaces:
// With authPlugin + dbPlugin + storagePlugin:type AppContext = { env: ParsedEnv<typeof envSchema>; auth: { user: AuthUser | null; grants: Grant[]; instance: AuthInstance }; db: { client: Db }; storage: { handle: HandleFn; getSignedUrl: SignedUrlFn };};Type-Safe Plugin Dependencies
Section titled “Type-Safe Plugin Dependencies”Plugin dependencies are declared via a TypeScript generic on definePlugin<TRequires>(). A curried form lets you specify requirements while the compiler infers the rest:
import { definePlugin } from "@cfast/core";import type { AuthPluginProvides } from "@cfast/auth";
export const dbPlugin = (config: DbPluginConfig) => definePlugin<AuthPluginProvides>()({ name: "db", setup(ctx) { // ctx.auth.user and ctx.auth.grants are typed return { client: createDb({ d1: ctx.env.DB, ... }) }; }, });If a required plugin renames a field, dependent plugins break at the type level. Multiple dependencies use intersection types: definePlugin<AuthPluginProvides & DbPluginProvides>()({ ... }).
Common Patterns
Section titled “Common Patterns”Simplified Route Loaders
Section titled “Simplified Route Loaders”Use app.context() directly or the convenience wrappers app.loader() and app.action():
// Direct usageexport async function loader({ request, context }: Route.LoaderArgs) { const ctx = await app.context(request, context); return ctx.db.client.query(posts).findMany().run({});}
// Convenience wrapper -- context is the first argumentexport const loader = app.loader(async (ctx, { params }) => { return ctx.db.client.query(posts).findMany().run({});});Client-Side Provider Composition
Section titled “Client-Side Provider Composition”<app.Provider> composes all plugin providers into a single tree. Plugins without a Provider are skipped:
import { app } from "~/cfast";
export function Layout({ children }: { children: React.ReactNode }) { return ( <html> <body> <app.Provider> {children} </app.Provider> </body> </html> );}Access all plugins’ client-side exports with useApp():
import { useApp } from "@cfast/core/client";
function MyComponent() { const { auth, storage } = useApp(); const user = auth.useCurrentUser(); const upload = storage.useUpload("avatar");}Individual package hooks (like useCurrentUser()) continue to work directly — useApp() is additive.
Writing a Custom Plugin
Section titled “Writing a Custom Plugin”Leaf plugins (no dependencies) use the direct form:
import { definePlugin } from "@cfast/core";
export const analyticsPlugin = (config: AnalyticsConfig) => definePlugin({ name: "analytics", setup(ctx) { // ctx has { request, env } only return { track: (event: string) => { /* ... */ } }; }, });Export a type token so other plugins can depend on yours:
import type { PluginProvides } from "@cfast/core";export type AnalyticsPluginProvides = PluginProvides<ReturnType<typeof analyticsPlugin>>;