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.

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

Updated