Pitlane

Platform Integration for Remix Apps

Running on Cloudflare and built around Vite+, explicit config, and typed runtime primitives.

$ vp create pitlane my-app
$ vp install
$ vp dev
$ pitlane resources create
$ pitlane deploy
Install
$vp create pitlane my-app

Scaffolds a Remix app wired for Vite+ and Pitlane on Cloudflare.

The Stack

One stack: Remix framework, Vite+ tooling, Cloudflare platform.

Why Pitlane

Cloudflare primitives that fit Remix instead of fighting it.

Pitlane keeps the platform visible and typed. Configure resources in platform(), let Pitlane generate Wrangler config and worker types, then read D1, R2, KV sessions, queues, and cron through explicit Remix middleware.

Vite+ runs the application lifecycle. Pitlane handles the Cloudflare platform work around it: provisioning, migrations, secrets, generated configuration, and deploys.

Learn the workflow
Primitives

Cloudflare platform primitives, designed for Remix.

D1 Database
pitlane/data-table-d1
import { env } from "cloudflare:workers";
import { D1DatabaseAdapter } from "pitlane/data-table-d1";
import { Database } from "remix/data-table";
import { createController } from "remix/router";

let adapter = new D1DatabaseAdapter(env.DB);
let db = new Database(adapter);

export default createController(routes.contacts, {
    actions: {
        async index({ render }) {
            let contacts = await db.findMany(Contacts);

            return await render(
                <Document breadcrumbs>
                    <Head>
                        <title>{`All Contacts (${contacts.length}) | Address Book`}</title>
                        <link
                            data-precedence="route"
                            href={styles}
                            rel="stylesheet"
                        />
                    </Head>
                    <ContactsList
                        contacts={contacts}
                    />
                </Document>,
            );
        },
    },
});
R2 File Storage
pitlane/file-storage-r2
import { env } from "cloudflare:workers";
import { R2FileStorage } from "pitlane/file-storage-r2";
import { createController } from "remix/router";

let files = new R2FileStorage(env.FILES);

export default createController(routes.avatar, {
    actions: {
        async upload({ request }) {
            await files.set(
                "avatar",
                await context.request.blob(),
            );
        },
    },
});
KV Sessions
pitlane/session-storage-kv
import { env } from "cloudflare:workers";
import { createKVSessionStorage } from "pitlane/session-storage-kv";
import { createController } from "remix/router";
import { Session } from "remix/session";
import { session } from "remix/session-middleware";

let storage = createKVSessionStorage(env.SESSIONS, {
    keyPrefix: "session:",
    ttl: 60 * 60 * 24,
});

export default createController(routes, {
    middleware: [session(cookie, storage)],
    actions: {
        async index({ session }) {
            return Response.json({
                count: session.get("count") ?? 0,
            });
        },
    },
});
Images
pitlane/assets
import parrotImage from "#/assets/images/parrot.png?url";
import robinImage from "#/assets/images/robin.png?url";
import { Image, Picture } from "pitlane/assets";

function Birds() {
    return () => (
        <>
            <Image
                src={robinImage}
                width={400}
                height={300}
                alt="A robin sitting on a nest of eggs."
            />
            <Picture
                src={parrotImage}
                width={400}
                height={300}
                formats={["avif", "webp"]}
                alt="A parrot sitting on a nest of eggs."
            />
        </>
    );
}
Content Definition
pitlane/content
import { createContent } from "pitlane/content";
import * as loaders from "pitlane/content/loaders";
import * as s from "remix/data-schema";

export let content = await createContent(c => ({
    blog: c.collection({
        loader: loaders.glob({
            pattern: "app/content/**/*.{md,mdx}",
            base: "blog",
        }),
        schema: s.object({
            title: s.string(),
            summary: s.string(),
            publishedOn: s.date(),
            author: c.reference("authors"),
        }),
    }),
    authors: c.collection({
        loader: loaders.file(
            "app/content/authors.jsonc",
        ),
        schema: s.object({
            name: s.string(),
            avatar: s.string(),
        }),
    }),
}));
Content Usage
pitlane/content
import { createController } from "remix/router";

import { content } from "./content.ts";

export default createController(routes.blog, {
    actions: {
        async show({ params, render }) {
            let posts =
                await content.blog.getCollection();
            let post = await content.blog.getEntry(
                params.slug,
            );
            if (!post)
                return await render(<NotFound />, {
                    status: 404,
                });

            let { Content, headings } =
                await post.render();
            let author =
                await content.authors.getEntry(
                    post.data.author,
                );

            return await render(
                <>
                    <h1>{post.data.title}</h1>
                    <h2>{post.data.dek}</h2>
                    <p>by {author.data.name}</p>
                    <main>
                        <Content />
                    </main>
                    <aside>
                        <ul>
                            {headings.map(heading => (
                                <li>{heading.text}</li>
                            ))}
                        </ul>
                    </aside>
                </>,
            );
        },
    },
});
Feature Flags
pitlane/flags
import { env } from "cloudflare:workers";
import { createFeatures, flags } from "pitlane/flags";
import * as flag from "pitlane/flags/schema";
import * as s from "remix/data-schema";
import { createController } from "remix/router";

let features = createFeatures({
    newCheckout: {
        name: "new-checkout",
        input: {
            userId: flag.header(
                "x-user-id",
                s.defaulted(s.string(), "anonymous"),
            ),
            plan: flag.header(
                "x-plan",
                s.defaulted(s.string(), "free"),
            ),
        },
        output: s.defaulted(s.boolean(), false),
    },
});

export default createController(routes.shop, {
    middleware: [flags(env.FLAGS)],
    actions: {
        async checkout({ headers, flags, render }) {
            let useNewCheckout = await flags.get(
                features.newCheckout,
            );
            if (useNewCheckout)
                return await render(<NewCheckout />);
            return await render(<Checkout />);
        },
    },
});
Realtime
pitlane/realtime
import { Realtime } from "pitlane/realtime";
import {
    addEventListeners,
    clientEntry,
} from "remix/ui";

let ChatPanel = clientEntry(
    import.meta.url,
    handle => {
        let realtime = handle.context.get(Realtime);

        addEventListeners(realtime, handle.signal, {
            chatmessage() {
                handle.update();
            },
            statuschange() {
                handle.update();
            },
        });

        return () => (
            <section>
                <ul>
                    {realtime.messages.map(message => (
                        <li key={message.id}>
                            {JSON.stringify(message)}
                        </li>
                    ))}
                </ul>
            </section>
        );
    },
);
Scheduled Jobs
pitlane/job
import { env } from "cloudflare:workers";
import {
    createJobs,
    Scheduler,
    createScheduledJobs,
} from "pitlane/job";

let jobs = createJobs({
    dailyDigest: {
        async handle() {
            await sendDailyDigest();
        },
    },
});

let scheduler = new Scheduler(jobs, {
    queue: env.TASKS,
});

let scheduled = createScheduledJobs(scheduler, {
    "0 0 * * *": jobs.dailyDigest,
});

export default {
    fetch: router.fetch,
    scheduled: scheduled.handler,
};
Background Jobs
pitlane/job
import { env } from "cloudflare:workers";
import {
    createJobs,
    Scheduler,
    createJobQueue,
} from "pitlane/job";
import * as s from "remix/data-schema";
import { redirect } from "remix/response/redirect";
import { createController } from "remix/router";

let jobs = createJobs({
    sendEmail: {
        schema: s.object({
            to: s.string(),
            subject: s.string(),
        }),
        async handle(payload) {
            await sendEmail(
                payload.to,
                payload.subject,
            );
        },
    },
});

let scheduler = new Scheduler(jobs, {
    queue: env.TASKS,
});

let emailController = createController(routes.email, {
    actions: {
        async create({ formData }) {
            let email = s.parse(EmailSchema, formData);

            await scheduler.enqueue(jobs.sendEmail, {
                to: email.address,
                subject: "Welcome to Pitlane",
            });

            return redirect(routes.home.href());
        },
    },
});

router.map(routes.email, emailController);

let queue = createJobQueue(scheduler);

export default {
    fetch: router.fetch,
    queue: queue.handler,
};
Workers AI
pitlane/ai
import { env } from "cloudflare:workers";
import { AI, createTool } from "pitlane/ai";
import { gemma } from "pitlane/ai-google";
import * as s from "remix/data-schema";
import { createController } from "remix/router";

let ai = new AI(env.AI);

let weather = createTool({
    name: "get_weather",
    description: "Get the weather in a location",
    schema: s.object({
        location: s.string(),
    }),
    async handle({ location }) {
        return await getWeather(location);
    },
});

export default createController(routes.messages, {
    actions: {
        async create({ formData }) {
            let { prompt } = s.parse(
                f.object({
                    prompt: f.field(
                        s.defaulted(
                            s.string(),
                            "What's the temperature in Dallas?",
                        ),
                    ),
                }),
                formData,
            );

            let stream = ai.stream({
                model: gemma("4-26b-a4b-it"),
                prompt,
                tools: [weather],
            });

            return new Response(stream, {
                headers: {
                    "Content-Type":
                        "text/event-stream",
                },
            });
        },
    },
});
<head> management
pitlane/metadata
import { Head } from "pitlane/metadata";
import { createController } from "remix/router";

import styles from "./assets/index.css?url";
import { SITE } from "./site/data.ts";

export default createController(routes.about, {
    actions: {
        async index({ render }) {
            let title = "About Me";

            return await render(
                <Document breadcrumbs>
                    <Head>
                        <title>{`${title} | ${SITE.title}`}</title>
                        <link
                            data-precedence="route"
                            href={styles}
                            rel="stylesheet"
                        />
                    </Head>
                    <AboutPage />
                </Document>,
            );
        },
    },
});
Lap Time

From scaffold to deploy in under one minute.

00:00.00$ vp create pitlane my-app→ Scaffolded my-app
 $ cd my-app && vp install→ Installed 412 packages
 $ pitlane resources create→ D1, KV, R2, Queue ready
 $ vp dev→ Local server on :1612
 $ pitlane deploy→ Live at https://my-app.workers.dev
Pitlane pit crew servicing a Pitlane-branded race carPitlane pit crew servicing a Pitlane-branded race car
Platform Ops

Platform ops at pit-crew speed.

pitlane resources create reads platform() and provisions D1, KV, R2, queues, and cron triggers in one pass. pitlane db migrate runs pending D1 migrations. pitlane secrets push syncs secrets from .env to Wrangler. pitlane deploy ties it all together.

CLI reference
Pole Position

A short setup, a long straight.

·3lines
To wire a Cloudflare primitive into a Remix route — import, mount middleware, read from context.
·330+
Edge cities your app runs in by default, on Cloudflare's global Workers network.
·0ms
Cold start. V8 isolates, not containers — your routes are warm everywhere.
·0
wrangler.toml files to maintain by hand. Pitlane generates them from platform().
Build With
Footer background

Start with Pitlane

Create a Remix app, configure Cloudflare resources, and deploy through the same Vite+ workflow you use every day.

Get started

© 2026 Pitlane contributors.