Tulip Logo IconTulip
Local Provider

Setup

Wire the local provider by combining storage logic, procedures, and provider components.

Required parts

  • a concrete local drive service implementation
  • a contract implementation created from localDriveRouterContract
  • a LocalDriveProvider on the client
  • optional upload client wiring for direct uploads

In practice, most screens also need:

  • a route handler for opening files
  • a query layer that feeds LocalDriveViewProvider
  • breadcrumbs derived from getFolderParents
  • mutation commands such as rename, create folder, archive, and delete

Main entrypoints

import { localDriveRouterContract } from "@tulip-systems/drive-local/server";
import { createLocalDriveRouteHandler } from "@tulip-systems/drive-local/server";
import { LocalDriveProvider } from "@tulip-systems/drive-local/client";
import { createLocalDriveUploadClient } from "@tulip-systems/drive-local/client";
import { implement } from "@orpc/server";
import type { RPCContext } from "@/server/router/init";

Use contract-first implementation with your app's own RPC context and middleware:

const localDriveProcedure = implement(localDriveRouterContract)
  .$context<RPCContext>()
  .use(sessionMiddleware);

Add the local drive schema

Define local-drive tables in your app so you control base columns and can add app-specific fields.

import { imageVariants, nodeSubtypes } from "@tulip-systems/drive-local";
import { imageDispositions } from "@tulip-systems/storage";
import { relations } from "drizzle-orm";
import { type AnyPgColumn, boolean, pgEnum, pgTable, unique } from "drizzle-orm/pg-core";
import { baseColumns } from "@/server/db/helpers";
import { storageAssets } from "@/server/storage/schema";

export const nodeTypeEnum = pgEnum("node_types", ["file", "folder"]);

export const nodes = pgTable("nodes", (t) => ({
  ...baseColumns,
  name: t.text().notNull(),
  namespace: t.text().notNull().default("global"),
  type: nodeTypeEnum(),
  subtype: t.text({ enum: nodeSubtypes }).notNull().default("other"),
  size: t.integer(),
  contentType: t.varchar({ length: 255 }),
  readonly: boolean().default(false),
  hidden: boolean().default(false),
  archivedAt: t.timestamp(),
  parentId: t.uuid().references((): AnyPgColumn => nodes.id, { onDelete: "cascade" }),
  assetId: t.uuid().references(() => storageAssets.id, { onDelete: "cascade" }),
}));

export const nodesRelations = relations(nodes, ({ one, many }) => ({
  parent: one(nodes, { fields: [nodes.parentId], references: [nodes.id], relationName: "parent" }),
  children: many(nodes, { relationName: "parent" }),
  urls: many(nodePresignedUrls),
  variants: many(nodeVariants),
}));

export const nodeVariants = pgTable("node_variants", (t) => ({
  ...baseColumns,
  nodeId: t.uuid().notNull().references(() => nodes.id, { onDelete: "cascade" }),
  assetId: t.uuid().notNull().references(() => storageAssets.id, { onDelete: "cascade" }),
  variant: t.text({ enum: imageVariants }).notNull(),
  width: t.integer().notNull(),
}));

export const nodeVariantsRelations = relations(nodeVariants, ({ one }) => ({
  node: one(nodes, { fields: [nodeVariants.nodeId], references: [nodes.id], relationName: "node" }),
}));

export const nodePresignedUrls = pgTable(
  "node_presigned_urls",
  (t) => ({
    ...baseColumns,
    url: t.text().notNull().unique(),
    variant: t.text({ enum: imageVariants }).notNull(),
    disposition: t.text({ enum: imageDispositions }).notNull(),
    expiresAt: t.timestamp().notNull(),
    nodeId: t.uuid().notNull().references(() => nodes.id, { onDelete: "cascade" }),
    variantId: t.uuid().references(() => nodeVariants.id, { onDelete: "set null" }),
  }),
  (t) => [unique("node_presigned_url_unique").on(t.nodeId, t.variant, t.disposition)],
);

export const nodePresignedUrlsRelations = relations(nodePresignedUrls, ({ one }) => ({
  node: one(nodes, { fields: [nodePresignedUrls.nodeId], references: [nodes.id], relationName: "node" }),
}));

Pass those tables to the Drizzle adapter when you create the service.

import { localDriveDrizzleAdapter } from "@tulip-systems/drive-local/drizzle";
import { LocalDrive } from "@tulip-systems/drive-local/server";
import { db } from "@/server/db/init";
import * as driveSchema from "@/server/drive-local/schema";
import { storage } from "@/server/storage/init";
import * as storageSchema from "@/server/storage/schema";

export const drive = new LocalDrive({
  database: localDriveDrizzleAdapter({
    db,
    schema: { ...driveSchema, storageAssets: storageSchema.storageAssets },
  }),
  storage,
});

Provider responsibilities

LocalDriveProvider composes selection, view state, drag and drop, permission checks, and upload-zone state in one place.

Important LocalDriveProvider props

The provider accepts more than just namespace.

Useful props include:

  • namespace
  • permission
  • meta
  • readonly
  • initialView
  • selection
  • selectionConditions
  • onMove
  • driveUploadClient
  • uploadHooks
  • onUploadCompleted
  • onUploadFailed
  • optimistic
  • disabled
  • parentId
  1. implement the server service
  2. implement the contract and expose route handlers
  3. build the upload client from those endpoints
  4. mount LocalDriveProvider
  5. feed LocalDriveViewProvider with data and commands

On this page