Overview
Every provider lives in packages/<provider-name>/ and is integrated into apps/api. Use packages/google-drive (OAuth) or packages/s3 (API key) as reference implementations.
Step 1 — Create the package
packages/<name>/package.json
{
"name": "@drivebase/<name>",
"version": "0.1.0",
"module": "index.ts",
"type": "module",
"private": true,
"description": "<Description>",
"exports": { ".": "./index.ts" },
"dependencies": {
"@drivebase/core": "workspace:*",
"zod": "^3.22.4"
},
"devDependencies": { "@types/bun": "latest" },
"peerDependencies": { "typescript": "^5" }
}
Add any provider-specific SDK dependency to dependencies.
packages/<name>/tsconfig.json
Copy verbatim from packages/google-drive/tsconfig.json.
Step 2 — Write schema.ts
Define a Zod schema, a TypeScript type, sensitive fields, and UI config fields.
import type { ProviderConfigField } from "@drivebase/core";
import { z } from "zod";
export const <Name>ConfigSchema = z.object({
// Fields needed before OAuth callback (e.g. clientId, clientSecret)
// Fields populated after OAuth callback are optional (refreshToken, accessToken)
});
export type <Name>Config = z.infer<typeof <Name>ConfigSchema>;
// Fields encrypted at rest — secrets and tokens
export const <Name>SensitiveFields = ["<field>"] as const;
// Fields shown to the user in the Connect Provider UI
// Tokens are NOT shown — they come from OAuth
export const <Name>ConfigFields: ProviderConfigField[] = [
{
name: "<field>",
label: "<Label>",
type: "text" | "password",
required: true,
description: "<Help text>",
placeholder: "<Placeholder>",
},
];
Rules:
- •NEVER use
anytype. - •All secrets go in
SensitiveFields(encrypted with AES-256-GCM). - •Tokens obtained via OAuth are
optional()in the schema; they must not appear inConfigFields.
Step 3 — Write oauth.ts (OAuth providers only)
import type { OAuthInitResult, ProviderConfig } from "@drivebase/core";
import { ProviderError } from "@drivebase/core";
export async function initiate<Name>OAuth(
config: ProviderConfig,
callbackUrl: string,
state: string,
): Promise<OAuthInitResult> { ... }
export async function handle<Name>OAuthCallback(
config: ProviderConfig,
code: string,
callbackUrl: string,
): Promise<ProviderConfig> {
// Exchange code for tokens, return { ...config, refreshToken, accessToken }
}
Rules:
- •Always request offline access so a
refresh_tokenis issued. - •Throw
ProviderError("<name>", "...")(never return null/undefined for errors). - •The callback URL is a fixed pre-registered redirect URI — use as-is, never construct it here.
- •The
stateparameter carries<providerId>:<csrfToken>— pass through unchanged.
Step 4 — Write provider.ts
Implement IStorageProvider from @drivebase/core.
import type { IStorageProvider, ProviderConfig, ... } from "@drivebase/core";
import { ProviderError } from "@drivebase/core";
export class <Name>Provider implements IStorageProvider {
async initialize(config: ProviderConfig): Promise<void> { ... }
async testConnection(): Promise<boolean> { ... }
async cleanup(): Promise<void> { ... }
async getQuota(): Promise<ProviderQuota> { ... }
async requestUpload(options: UploadOptions): Promise<UploadResponse> { ... }
async uploadFile(remoteId: string, data: ReadableStream | Buffer): Promise<void> { ... }
async requestDownload(options: DownloadOptions): Promise<DownloadResponse> { ... }
async downloadFile(remoteId: string): Promise<ReadableStream> { ... }
async createFolder(options: CreateFolderOptions): Promise<string> { ... }
async delete(options: DeleteOptions): Promise<void> { ... }
async move(options: MoveOptions): Promise<void> { ... }
async copy(options: CopyOptions): Promise<string> { ... }
async list(options: ListOptions): Promise<ListResult> { ... }
async getFileMetadata(remoteId: string): Promise<FileMetadata> { ... }
async getFolderMetadata(remoteId: string): Promise<FolderMetadata> { ... }
// Non-standard, called by the service after OAuth to store account info
async getAccountInfo(): Promise<{ email?: string; name?: string }> { ... }
}
remoteId strategy
The remoteId is the permanent identifier stored in the database for each file/folder.
- •Prefer stable IDs (e.g.
id:abc123in Dropbox, numeric IDs in Box) over mutable paths. - •If the API requires a path for uploads (e.g. Dropbox), use
path_loweras remoteId throughout for consistency. - •The
fileIdreturned byrequestUploadis stored asremoteIdin the DB — make sure it's usable byuploadFileas-is.
Upload flow
- •Direct (presigned URL): return
{ fileId, uploadUrl, uploadFields, useDirectUpload: true }— the client uploads directly. - •Proxy: return
{ fileId, uploadUrl: undefined, useDirectDownload: false }— the server streams through/api/upload/proxy. - •Set
supportsPresignedUrls: falsein the registration when all uploads are proxied.
Download flow
- •Direct URL: call the provider's "temporary link" / presigned URL endpoint in
requestDownloadand return{ downloadUrl, useDirectDownload: true }. - •Proxy: return
{ downloadUrl: undefined, useDirectDownload: false }and implementdownloadFilefor streaming.
OAuth pending state
async initialize(config: ProviderConfig): Promise<void> {
// validate config
if (!this.config.refreshToken) return; // pending OAuth — skip client setup
// otherwise build the API client
}
async testConnection(): Promise<boolean> {
if (!this.config?.refreshToken) return false; // not throws — pending state
// ...
}
Token refresh
Refresh access tokens in-memory. On a 401 response, refresh and retry once. Do NOT persist the new access token back to the DB (no mechanism for that in providers).
Error handling
- •Use
ProviderError("<provider-type>", "message", { ...context })for all provider errors. - •NEVER use non-null assertions (
!). CallensureInitialized()private helper and throw if null. - •Validate config with
Schema.safeParseininitialize, throwProviderErroron failure.
cleanup()
Null out all client references so GC can reclaim them:
async cleanup(): Promise<void> {
this.client = null;
this.config = null;
}
Step 5 — Write index.ts
Export everything and define the ProviderRegistration:
export { handle<Name>OAuthCallback, initiate<Name>OAuth } from "./oauth"; // OAuth only
export { <Name>Provider } from "./provider";
export type { <Name>Config } from "./schema";
export { <Name>ConfigFields, <Name>ConfigSchema, <Name>SensitiveFields } from "./schema";
import type { ProviderRegistration } from "@drivebase/core";
// ... imports
export const <camelName>Registration: ProviderRegistration = {
factory: () => new <Name>Provider(),
configSchema: <Name>ConfigSchema,
configFields: <Name>ConfigFields,
description: "<Human-readable description>",
supportsPresignedUrls: true | false,
authType: "oauth" | "api_key" | "no_auth",
initiateOAuth: initiate<Name>OAuth, // OAuth only
handleOAuthCallback: handle<Name>OAuthCallback, // OAuth only
};
Step 6 — Add the provider type to core
In packages/core/enums.ts, add to ProviderType:
<NAME> = "<name>",
Step 7 — Add the DB enum value
In packages/db/schema/providers.ts:
export const providerTypeEnum = pgEnum("provider_type", [
"google_drive", "s3", "local", "dropbox", "<name>", // add here
]);
Step 8 — Create a DB migration
Create packages/db/migrations/<index>_add_<name>_provider.sql:
ALTER TYPE "public"."provider_type" ADD VALUE '<name>';
Update packages/db/migrations/meta/_journal.json — add a new entry with the next idx:
{
"idx": <next>,
"version": "7",
"when": <timestamp>,
"tag": "<index>_add_<name>_provider",
"breakpoints": true
}
Step 9 — Register in the API
apps/api/config/providers.ts
- •Import the registration and sensitive fields from
@drivebase/<name>. - •Add to
providerRegistry:[ProviderType.<NAME>]: <camelName>Registration - •Add to
providerSensitiveFields:[ProviderType.<NAME>]: <Name>SensitiveFields - •Add a
caseingetProviderName()returning the display name.
apps/api/package.json
Add "@drivebase/<name>": "workspace:*" to dependencies.
Step 10 — Install and verify
bun install bunx tsc --noEmit -p packages/<name>/tsconfig.json bunx tsc --noEmit -p apps/api/tsconfig.json 2>&1 | grep -i <name>
Zero errors from the new package. Pre-existing errors in the API are acceptable.
Step 11 — Update the README
In README.md, find the ## Supported Providers section and mark the new provider as done:
- [x] <Provider Display Name> ← change [ ] to [x], move above unchecked items
Keep the list ordered: completed providers first, then planned ones.
Step 12 — Add a docs page
Create apps/www/content/docs/storage-providers/<name>.mdx
Structure the page to match existing provider docs:
--- title: <Provider Display Name> description: Connect <Provider> accounts using <auth method> --- <One-line summary of what this provider does.> ## Prerequisites <Steps to create credentials in the provider's developer console.> ### 1. <First setup step> ### 2. <Second setup step — permissions/scopes> ### 3. Set the Redirect URI - Development: `http://localhost:4000/webhook/callback` - Production: `https://api.yourdomain.com/webhook/callback` ### 4. Note Your Credentials — list the fields the user will paste into Drivebase ## Connecting in Drivebase 1. Navigate to the **Providers** page. 2. Click **Connect Provider** and select **<Name>**. 3. Enter your credentials. 4. Click **Connect** and complete the authorization flow. <Callout type="info"> Any important notes about token storage, scopes, or limitations. </Callout> ## Notes - Any remoteId strategy, upload/download method, or behavioral quirks worth documenting.
Rules:
- •Always include the exact redirect URI format for both dev and prod.
- •Mention which fields are sensitive / encrypted.
- •If uploads go through the Drivebase proxy (no presigned URLs), say so explicitly.
- •If downloads use temporary/presigned links, describe the expiry.
Register the page in apps/www/content/docs/storage-providers/meta.json
Append the slug to the pages array:
{ "pages": ["overview", "local", "s3", "google-drive", ..., "<name>"] }
Add the provider to the overview page
In apps/www/content/docs/storage-providers/overview.mdx, add a bullet under Supported Providers:
- **[<Display Name>](/docs/storage-providers/<name>)**: <One-line description.>
Checklist
- •
packages/<name>/package.json— correct name,workspace:*deps - •
packages/<name>/tsconfig.json— copied from google-drive - •
schema.ts— Zod schema, sensitive fields, UI fields - •
oauth.ts— initiate + callback handlers (OAuth providers) - •
provider.ts— allIStorageProvidermethods implemented,getAccountInfo()present - •
index.ts— registration exported - •
packages/core/enums.ts—ProviderTypeupdated - •
packages/db/schema/providers.ts—providerTypeEnumupdated - • DB migration SQL file created
- •
_journal.jsonupdated - •
apps/api/config/providers.ts— provider registered - •
apps/api/package.json— dependency added - •
bun install— clean - •
bunx tsc --noEmit— zero new errors - •
README.md— provider checked off in Supported Providers - •
apps/www/content/docs/storage-providers/<name>.mdx— docs page created - •
apps/www/content/docs/storage-providers/meta.json— slug added topages - •
apps/www/content/docs/storage-providers/overview.mdx— bullet added