Tinybird Forward is live! See the Tinybird Forward docs to learn more. To migrate, see Migrate from Classic.
Multi-tenant real-time APIs with Clerk and Tinybird¶
In this guide, you'll learn how to build a multi-tenant real-time API with Clerk and Tinybird.
You can view the live demo or browse the GitHub repo (clerk-tinybird).
Prerequisites¶
This guide assumes that you have a Tinybird account, and you are familiar with creating a Tinybird Workspace and pushing resources to it.
You'll need a working familiarity with Clerk and Next.js.
Run the demo¶
These steps cover running the GitHub demo locally. Skip to the next section for a breakdown of the code.
- Clone the GitHub repo (clerk-tinybird) to your local machine.
- Create a new Clerk application
- Create a new Tinybird workspace
- Run
npm install
Configure .env¶
First create a new file .env.local
cp .env.example .env.local
Copy your Tinybird host, admin Token (used as the TINYBIRD_JWT_SECRET
) and Workspace ID to the .env.local
file:
# Tinybird API URL (replace with your Tinybird region host) NEXT_PUBLIC_TINYBIRD_API_URL=https://api.tinybird.co # Tinybird workspace ID for multi-tenant JWT tokens TINYBIRD_WORKSPACE_ID= # Tinybird workspace admin token for multi-tenant JWT tokens TINYBIRD_JWT_SECRET= # Tinybird default key for unauthenticated requests NEXT_PUBLIC_TINYBIRD_API_KEY= # Clerk publishable key NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY= # Clerk secret key CLERK_SECRET_KEY= # Clerk sign in URL NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/ NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/
If you want to allow anonymous requests, you can set NEXT_PUBLIC_TINYBIRD_API_KEY
to a key with read access to your Workspace.
Finally, create a new Clerk application and copy the publishable key and secret key to the .env.local
file.
Replace the Tinybird API hostname or region with the API region that matches your Workspace.
Run the demo app¶
Run it locally:
npm run dev
Then open localhost:3000
with your browser.
When you sign in, you'll see a Tinybird JWT token, and its decoded payload.
... "fixed_params": { "user_id": "user_2uJTrMdFeEI5icjxciCCsf0tKtf", "org_permission": "" } ...
You achieve multi-tenancy by:
- Including the Clerk user ID and/or other user/org properties in the JWT payload. This way, each user gets their own JWT, and can only access their own data.
- Using Tinybird templates to include parameters in your API endpoints.
For the demo app, the JWT payload includes the Clerk user ID and org name. The Tinybird API endpoint uses a template to include the user_id
and org_name
parameters in the endpoint.
SELECT * FROM "your_pipe" WHERE user_id = {{String(user_id)}} AND org_name = {{String(org_permission)}}
Learn more about Clerk organizations, roles and permissions in the Clerk docs.
Understand the code¶
This section breaks down the key parts of code from the example.
.env¶
The .env
file contains the environment variables used in the application.
middleware.ts¶
The middleware.ts
file contains the logic to generate and sign JWTs.
It uses a clerkMiddleware
function to check if the user is authenticated. If they are, it generates a JWT with the user's ID and org properties in the payload. For this specific case, if not logged in, it returns a default process.env.NEXT_PUBLIC_TINYBIRD_API_KEY
.
The code belows shows the function to sign the JWT. A JWT has various required fields, you can customize the payload as needed, including the expiration time, scopes, and fixed parameters.
middleware.ts
import { clerkMiddleware } from "@clerk/nextjs/server" import { NextResponse } from "next/server" import * as jose from 'jose' export default clerkMiddleware(async (auth) => { const authentication = await auth() const { userId, sessionId, orgPermissions } = authentication // If user is not authenticated, continue without modification if (!userId || !sessionId) { console.log('No user or session found') const response = NextResponse.next() response.headers.set('x-tinybird-token', process.env.NEXT_PUBLIC_TINYBIRD_API_KEY || '') return response } try { const orgName = orgPermissions?.[0]?.split(':').pop() || '' // Create Tinybird JWT const secret = new TextEncoder().encode(process.env.TINYBIRD_JWT_SECRET) const token = await new jose.SignJWT({ workspace_id: process.env.TINYBIRD_WORKSPACE_ID, name: `frontend_jwt_user_${userId}`, exp: Math.floor(Date.now() / 1000) + (60 * 15), // 15 minute expiration iat: Math.floor(Date.now() / 1000), scopes: [ { type: "PIPES:READ", resource: "your_pipe", fixed_params: { user_id: userId, org_permission: orgName } } ], limits: { rps: 10 } }) .setProtectedHeader({ alg: 'HS256' }) .sign(secret) // Clone the response and add token const response = NextResponse.next() response.headers.set('x-tinybird-token', token) response.headers.set('x-org-name', orgName) return response } catch (error) { console.error('Middleware error:', error) const response = NextResponse.next() response.headers.set('x-tinybird-token', process.env.NEXT_PUBLIC_TINYBIRD_API_KEY || '') return response } }) export const config = { matcher: [ // Skip Next.js internals and all static files, unless found in search params '/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)', // Always run for API routes '/(api|trpc)(.*)', ], }
Learn how to protect routes with Clerk in the Clerk docs.
layout.tsx¶
The layout.tsx
file is a server component that contains the Clerk provider and the Tinybird provider.
It takes the generated JWT from the clerkMiddleware
function from the page headers and passes it to the TinybirdProvider
as initialToken
and initialOrgName
.
layout.tsx
import { headers } from 'next/headers' import { Inter } from 'next/font/google' import { ClerkProvider } from '@clerk/nextjs' import { TinybirdProvider } from './providers/TinybirdProvider' import { RootLayoutContent } from './components/RootLayoutContent' import './globals.css' const inter = Inter({ subsets: ['latin'] }) export default async function RootLayout({ children, }: { children: React.ReactNode }) { const headersList = await headers() const token = headersList.get('x-tinybird-token') || '' const orgName = headersList.get('x-org-name') || '' return ( <html lang="en"> <body className={inter.className}> <ClerkProvider> <TinybirdProvider> <RootLayoutContent initialToken={token} initialOrgName={orgName}> {children} </RootLayoutContent> </TinybirdProvider> </ClerkProvider> </body> </html> ) }
RootLayoutContent.tsx¶
The RootLayoutContent.tsx
file is a client component that contains the Tinybird provider.
It takes the initialToken
and initialOrgName
from the RootLayout
server component and passes them to the TinybirdProvider
as initialToken
and initialOrgName
.
RootLayoutContent.tsx
'use client' import { ReactNode } from 'react' import { useTinybirdToken } from '../providers/TinybirdProvider' interface RootLayoutContentProps { children: ReactNode initialToken: string initialOrgName: string } export function RootLayoutContent({ children, initialToken, initialOrgName }: RootLayoutContentProps) { const { setToken, setOrgName } = useTinybirdToken() // Set the initial values from the server setToken(initialToken) setOrgName(initialOrgName) return <>{children}</> }
page.tsx¶
page.tsx
contains the main logic for the Next.js page. For this template project it gets the token from the useTinybirdToken
hook and renders the JWT payload including the Clerk user info.
page.tsx
'use client' import { SignInButton, SignOutButton, SignedIn, SignedOut } from '@clerk/nextjs' import { jwtDecode } from 'jwt-decode' import { Roboto_Mono } from 'next/font/google' import { useTinybirdToken } from './providers/TinybirdProvider' const robotoMono = Roboto_Mono({ subsets: ['latin'] }) export default function Home() { const { token } = useTinybirdToken() let decodedToken = null try { decodedToken = token ? jwtDecode(token) : null } catch (error) { console.error('Error decoding token:', error) } return ( <main className={`min-h-screen p-8 ${robotoMono.className}`}> <div className="max-w-4xl mx-auto space-y-8"> <div className="flex justify-between items-center"> <h1 className="text-2xl font-bold">Multi-tenancy with Clerk + Tinybird JWT</h1> <SignedOut> <SignInButton mode="modal"> <button className="relative bg-[#27f795] hover:bg-[#27f795] text-black px-4 py-2 group"> <span className="absolute left-[-5px] top-[-5px] h-2 w-2 border-l-2 border-t-2 border-transparent group-hover:border-[#27f795] transition-all duration-500 origin-top-left scale-0 group-hover:scale-100"></span> <span className="absolute right-[-5px] top-[-5px] h-2 w-2 border-r-2 border-t-2 border-transparent group-hover:border-[#27f795] transition-all duration-500 origin-top-right scale-0 group-hover:scale-100"></span> <span className="absolute left-[-5px] bottom-[-5px] h-2 w-2 border-l-2 border-b-2 border-transparent group-hover:border-[#27f795] transition-all duration-500 origin-bottom-left scale-0 group-hover:scale-100"></span> <span className="absolute right-[-5px] bottom-[-5px] h-2 w-2 border-r-2 border-b-2 border-transparent group-hover:border-[#27f795] transition-all duration-500 origin-bottom-right scale-0 group-hover:scale-100"></span> Sign In </button> </SignInButton> </SignedOut> <SignedIn> <SignOutButton> <button className="relative bg-[#27f795] hover:bg-[#27f795] text-black px-4 py-2 group"> <span className="absolute left-[-5px] top-[-5px] h-2 w-2 border-l-2 border-t-2 border-transparent group-hover:border-[#27f795] transition-all duration-500 origin-top-left scale-0 group-hover:scale-100"></span> <span className="absolute right-[-5px] top-[-5px] h-2 w-2 border-r-2 border-t-2 border-transparent group-hover:border-[#27f795] transition-all duration-500 origin-top-right scale-0 group-hover:scale-100"></span> <span className="absolute left-[-5px] bottom-[-5px] h-2 w-2 border-l-2 border-b-2 border-transparent group-hover:border-[#27f795] transition-all duration-500 origin-bottom-left scale-0 group-hover:scale-100"></span> <span className="absolute right-[-5px] bottom-[-5px] h-2 w-2 border-r-2 border-b-2 border-transparent group-hover:border-[#27f795] transition-all duration-500 origin-bottom-right scale-0 group-hover:scale-100"></span> Sign Out </button> </SignOutButton> </SignedIn> </div> <SignedIn> <div className="space-y-6"> <div className="bg-[#151515] p-4 rounded"> <h2 className="font-semibold mb-2">Tinybird Token:</h2> <pre className="bg-[#151515] p-2 rounded overflow-x-auto">{token}</pre> </div> {decodedToken && ( <div className="bg-[#151515] p-4 rounded"> <h2 className="font-semibold mb-2">Decoded Token:</h2> <pre className="bg-[#151515] p-2 rounded overflow-x-auto"> {JSON.stringify(decodedToken, null, 2)} </pre> </div> )} <div className="bg-[#151515] p-4 rounded"> <h2 className="font-semibold mb-2">Example Tinybird Request:</h2> <pre className="bg-[#151515] p-2 rounded overflow-x-auto"> {`fetch('https://api.tinybird.co/v0/pipes/your_pipe.json', { headers: { Authorization: 'Bearer ${token}' } })`} </pre> </div> </div> </SignedIn> </div> </main> ) }
You can use the generated JWT to fetch data from Tinybird. This way you can build a multi-tenant real-time API with Clerk and Tinybird.
Next steps¶
- Read the blog post on JWTs.
- Explore more use cases that use this approach, like building a real-time, user-facing dashboard.