Self-hosted auth + database platform for Next.js. Drop it in, configure your providers from a dashboard, and connect your app with a single SDK call.
Think: self-hosted Supabase / Clerk — you own the data, you control the infra.
| SDK | Language | Install |
|---|---|---|
| postbasejs | JavaScript / TypeScript | npm install postbasejs |
| postbasepy | Python | pip install postbasepy |
| postbasefl | Dart / Flutter | flutter pub add postbasefl |
|
Deploy on Railway |
Railway — tested, recommended. Two services (app + managed Postgres), one required secret. See RAILWAY.md. |
|
Deploy to DigitalOcean |
DigitalOcean — tested, recommended. App Platform + managed database, provisioned from the app spec. See DIGITALOCEAN.md. |
|
Deploy to Render |
Render — unverified. Blueprint ( |
|
Deploy to Fly.io |
Fly.io — unverified. Button only runs |
|
Deploy to AWS |
AWS — unverified. CloudFormation: CodeBuild builds the image, deploys to App Runner + RDS Postgres. See AWS.md. |
|
Run on Google Cloud |
Google Cloud — unverified. Opens Cloud Shell; |
|
Deploy to Azure |
Azure — unverified. ARM/Bicep: ACR Task builds the image, deploys to Container Apps + Postgres Flexible Server. See AZURE.md. |
|
Deploy to Oracle Cloud |
Oracle Cloud — unverified, extra setup required (GitHub token, manual |
Self-hosted auth + database platform for Next.js
Dashboard — manage organisations and projects
Project overview with quick-start guide
25+ auth providers — toggle any from the dashboard
Built-in SQL editor with AI query generation
S3-compatible storage — connect Amazon S3, Cloudflare R2, Backblaze B2, and more
Scheduled cron jobs — run SQL snippets or HTTP requests on any schedule
API keys — anon and service role keys with SDK snippet
Project settings — configure auth redirect URLs, JWT expiry, and more
- 25+ Auth Providers — Google, GitHub, Discord, Magic Link, Passkeys, SMS OTP, SAML/SSO, and more — all toggleable from the dashboard
- Database API — Query your PostgreSQL via
anon key(respects RLS) orservice_role key(full access) - JOIN & Raw SQL —
JOINacross tables or run raw SQL via the SDK; AI-powered SQL editor with human-readable result summaries - Table Explorer — Inline cell editing, column filtering, full-text row search, and CSV import directly from the dashboard
- RLS Policies — Row Level Security policy editor with live preview
- File Storage — S3-compatible object storage with bucket policies
- Transactional Email — Send email via SMTP or AWS SES (IAM keys or SMTP credentials), configured per-project from the dashboard
- Cron Jobs — SQL or HTTP jobs on any schedule; full run history with request/response detail panel, date filter, and bulk delete
- Multi-project — One Postbase instance can serve multiple apps
- Self-hosted — Single
docker compose upand you're running
git clone https://github.com/harshalone/postbase
cd postbase
cp .env.example .envGenerate and set a secret in .env:
openssl rand -base64 32 # paste output as NEXTAUTH_SECRETdocker compose up -dThis starts:
- PostgreSQL on port
5432 - Postbase app on port
3000
cd apps/web
pnpm install
pnpm db:pushVisit http://localhost:3000/dashboard
- Create a project → get your
anon keyandservice_role key - Go to Auth Providers → enable the providers you want, paste in OAuth credentials
- Copy your keys from the API Keys tab
For development you don't need to rebuild Docker on every change. Run only the infrastructure (PostgreSQL) in Docker and the Next.js app locally with hot reload.
The quickest way is the included dev.sh script:
./dev.sh # start infra + app (hot reload)
./dev.sh --rebuild # rebuild containers
./dev.sh --reset # wipe data and restart cleanOr manually:
pnpm infra:upThis starts PostgreSQL in Docker — without the app container.
pnpm db:push # first time only — run migrations
pnpm dev # Next.js dev server with hot reloadThat's it. Edit code → changes reflect instantly, no Docker rebuild needed.
| Command | Description |
|---|---|
pnpm infra:up |
Start postgres |
pnpm infra:down |
Stop postgres (data is preserved) |
pnpm infra:logs |
Tail infrastructure logs |
pnpm dev |
Start Next.js dev server |
pnpm db:push |
Push schema changes to the database |
pnpm db:studio |
Open Drizzle Studio (visual DB browser) |
When deploying, use the full Docker Compose stack which includes the app container:
docker compose up -dnpm install postbasejs
# or
pnpm add postbasejs// lib/postbase.ts
import { createClient } from 'postbasejs'
export const postbase = createClient(
'http://localhost:3000', // your Postbase instance URL
'pb_anon_...', // your anon key (safe for browser)
{ projectId: 'your-project-id' }
)For server-side / admin operations use your service_role key — keep it out of the browser.
await postbase.auth.signUp({ email: 'user@example.com', password: 'secret' })
const { data, error } = await postbase.auth.signInWithPassword({
email: 'user@example.com',
password: 'secret',
})
await postbase.auth.signOut()
const { data: { session } } = await postbase.auth.getSession()
postbase.auth.onAuthStateChange((event, session) => {
console.log(event, session?.user)
})Remember me (extended session TTL): pass rememberMe: true to signUp, signInWithPassword,
verifyOtp, verifyEmailOtp, or signInWithIdToken to issue a 30-day refresh token instead of the
default 7-day one. The flag is preserved automatically on every later refresh.
await postbase.auth.signInWithPassword({ email, password, rememberMe: true })Browser OAuth redirects (signInWithOAuth + handleOAuthCallback) have no request body at sign-in
time, so rememberMe can't be passed in directly — call postbase.auth.setRememberMe(true) after
handleOAuthCallback() resolves instead. See the SKILL reference for the full pattern.
// Redirects browser to the provider, then back to your redirectTo URL
await postbase.auth.signInWithOAuth({
provider: 'google', // 'github', 'discord', 'apple', etc.
options: { redirectTo: 'https://yourapp.com/callback' },
})
// On your callback page — parses tokens from the URL automatically
const { data, error } = await postbase.auth.handleOAuthCallback()Use an in-app browser (ASWebAuthenticationSession on iOS, Chrome Custom Tab on Android) with a custom URL scheme as the redirect target:
// Returns the authorize URL for you to open in an in-app browser
const authorizeUrl = await postbase.auth.signInWithOAuth({
provider: 'github',
options: { redirectTo: 'com.myapp://auth/callback' },
})
// → open authorizeUrl in ASWebAuthenticationSession / Chrome Custom Tab
// After the in-app browser hands the URL back to your app:
const { data, error } = await postbase.auth.handleOAuthCallback({
url: incomingUrl, // e.g. 'com.myapp://auth/callback?access_token=...'
})For apps that use ASAuthorizationController (Apple) or GIDSignIn (Google), pass the id_token directly — no browser, no redirect:
// Apple (Swift → bridge identityToken string to JS)
const { data, error } = await postbase.auth.signInWithIdToken({
provider: 'apple',
idToken: appleIdentityToken,
nonce: nonce, // optional, if you passed one to ASAuthorizationAppleIDRequest
rememberMe: true, // optional, 30-day refresh token instead of the default 7-day one
})
// Google (Android / iOS)
const { data, error } = await postbase.auth.signInWithIdToken({
provider: 'google',
idToken: googleIdToken,
})
// data.session.accessToken, data.session.refreshToken, data.user// SELECT
const { data, error } = await postbase
.from('posts')
.select('id, title, created_at')
.eq('user_id', userId)
.order('created_at', { ascending: false })
.limit(10)
// INSERT
const { data } = await postbase
.from('posts')
.insert({ title: 'Hello world', user_id: userId })
.select()
.single()
// UPDATE
await postbase.from('posts').update({ title: 'Updated' }).eq('id', postId)
// DELETE
await postbase.from('posts').delete().eq('id', postId)
// OR filters
const { data } = await postbase
.from('posts')
.select('*')
.orFilters([{ column: 'status', op: 'eq', value: 'draft' }, { column: 'status', op: 'eq', value: 'published' }])
// JOIN across tables
const { data } = await postbase
.from('posts')
.select('id, title, users(name, email)')
.join({ table: 'users', on: 'posts.user_id = users.id' })
// Raw SQL (service_role key required)
const { data } = await postbase.sql('SELECT count(*) FROM posts WHERE created_at > $1', ['2024-01-01'])anon key — enforces Row Level Security policies on your tables. service_role key — bypasses RLS. Server-side only.
Every project schema (proj_<uuid-no-dashes>) is auto-provisioned with four auth tables before you ever run SQL yourself: users, accounts, sessions, verification_tokens. They aren't global — they live inside your own project schema — but they already exist, so CREATE TABLE accounts (...) (or users/sessions/verification_tokens) will fail with relation "accounts" already exists. Name your own tables something else (e.g. profiles, my_accounts) and sync from users via a trigger if you need to extend the built-in user record — see the Claude Code skill for the pattern.
// Upload a file
const { data, error } = await postbase
.storage
.from('avatars')
.upload('user-123/avatar.png', file)
// Get a public URL
const { data: { publicUrl } } = postbase.storage.from('avatars').getPublicUrl('user-123/avatar.png')
// Download
const { data: blob } = await postbase.storage.from('avatars').download('user-123/avatar.png')
// List files
const { data: files } = await postbase.storage.from('avatars').list('user-123/')
// Delete
await postbase.storage.from('avatars').remove(['user-123/avatar.png'])Send transactional email through the project's configured provider (SMTP or AWS SES) — configure it from the dashboard's Auth → Email settings.
const { data, error } = await postbase.email.send({
to: 'user@example.com',
subject: 'Welcome!',
text: 'Hello there',
html: '<p>Hello there</p>',
})
// data.okEnable any of these from the dashboard — no code changes needed.
| Category | Providers |
|---|---|
| Social | Google, GitHub, Discord, Twitter/X, Facebook, LinkedIn, Apple, Microsoft, Slack, Twitch, Spotify, Notion, GitLab, Bitbucket, Dropbox, Box |
| Credentials | Email + Password, Magic Link, Phone/SMS OTP |
| Passwordless | Passkeys (WebAuthn), Anonymous/Guest |
| Enterprise | SAML/SSO, Okta, Keycloak, Auth0 |
Apple and Google additionally support a native id_token flow — iOS/macOS/Android apps can sign users in via the OS native SDK (no browser required) using signInWithIdToken() in the JS SDK.
| Variable | Description | Default |
|---|---|---|
DATABASE_URL |
PostgreSQL connection string | postgresql://postbase:postbase@localhost:5432/postbase |
NEXTAUTH_SECRET |
Secret for signing tokens — required | — |
NEXTAUTH_URL |
Public URL of your Postbase instance | http://localhost:3000 |
- Next.js 15 — dashboard + API
- Auth.js v5 — auth provider handling
- Drizzle ORM — database schema & queries
- PostgreSQL 16 — primary database
- Docker — containerized deployment
A Claude Code skill is bundled at skills/postbase/SKILL.md.
It covers the full Postbase API reference, postbasejs SDK patterns, RLS, auth tables, cron jobs, storage, and Swift integration — loaded automatically when you use /postbase in Claude Code, or activated whenever Claude detects you're building against a Postbase backend.
To install it in your own project:
cp skills/postbase/SKILL.md <your-project>/.claude/skills/postbase/SKILL.mdMIT
