Launch Week 12: Day 3

Learn more

Cal.com launches Expert Marketplace built with Next.js and Supabase.

2024-06-18

7 minute read

Contrary to popular belief, due to the biggest online beef marketing campaign in modern history, Cal.com and Supabase are actually supa good friends, united by a common mission to build open source software.

So when the Cal.com team reached out about collaborating on their new platform starter kit, we were excited to work together. Finally we could collaborate on a Product Hunt launch instead of competing against each other.

What's the stack?

Initially the application was built to be run on SQLite. However, once requirements grew to include file storage, the Cal.com team remembered their Supabase frenemies and luckily, thanks to Prisma and Supabase, switching things over to Postgres three days before launch was a breeze.

Prisma configuration for usage with Postgres on Supabase

When working with Prisma, your application will connect directly to your Postgres databases hosted on Supabase. To handle connection management efficiently, especially when working with serverless applications like Next.js, Supabase provides a connection pooler called Supavisor to make sure your database runs efficiently with increasing traffic.

The configuration is specified in the schema.prisma file where you provide the following connection strings:

schema.prisma

_10
datasource db {
_10
provider = "postgresql"
_10
url = env("POSTGRES_PRISMA_URL")
_10
directUrl = env("POSTGRES_URL_NON_POOLING")
_10
schemas = ["prisma"] // see multi-schema support below
_10
}

This loads the relevant Supabase connections strings from your .env file

.env

_10
POSTGRES_PRISMA_URL="postgres://postgres.YOUR-PROJECT-REF:[YOUR-PASSWORD]@aws-0-[REGION].pooler.supabase.com:6543/postgres?pgbouncer=true&connection_limit=1" # Transaction Mode
_10
POSTGRES_URL_NON_POOLING="postgres://postgres.YOUR-PROJECT-REF:[YOUR-PASSWORD]@aws-0-[REGION].pooler.supabase.com:5432/postgres" # Session Mode

You can find the values in the Database settings of your Supabase Dashboard.

For more details on using Prisma with Supabase, read the official docs.

Multischema support in Prisma

In Supabase the public schema is exposed via the autogenerated PostgREST API, which allows you to connect with your database from any environment that speaks HTTPS using the Supabase client libraries like supabase-js for example.

Since Prisma connects directly to your database, it's advisable to put your data on a separate schema that is not exposed via the API.

We can do this by enabling multischema support in the schema.prisma file:

schema.prisma

_11
generator client {
_11
provider = "prisma-client-js"
_11
previewFeatures = ["multiSchema"]
_11
}
_11
_11
model Account {
_11
id String @id @default(cuid())
_11
// ...
_11
_11
@@schema("prisma")
_11
}

React Dropzone and Supabase Storage for profile image uploads

Supabase Storage is an S3 compatible cloud-based object store that allows you to store files securely. It is conveniently integrated with Supabase Auth allowing you to easily limit access for uploads and downloads.

Cal.com's Platforms Starter Kit runs their authentication on Next.js' Auth.js. Luckily though, Supabase Storage is supa flexible, allowing you to easily create signed upload URLs server-side to then upload assets from the client-side -- no matter which tech you choose to use for handling authentication for your app.

To facilitate this, we can create an API route in Next.js to generate these signed URLs:

src/app/api/supabase/storage/route.ts

_31
import { auth } from '@/auth'
_31
import { env } from '@/env'
_31
import { createClient } from '@supabase/supabase-js'
_31
_31
export const dynamic = 'force-dynamic' // defaults to auto
_31
export async function GET(request: Request) {
_31
try {
_31
const session = await auth()
_31
if (!session || !session.user.id) {
_31
return new Response('Unauthorized', { status: 401 })
_31
}
_31
const {
_31
user: { id },
_31
} = session
_31
// Generate signed upload url to use on client.
_31
const supabaseAdmin = createClient(env.NEXT_PUBLIC_SUPABASE_URL, env.SUPABASE_SERVICE_ROLE_KEY)
_31
_31
const { data, error } = await supabaseAdmin.storage
_31
.from('avatars')
_31
.createSignedUploadUrl(id, { upsert: true })
_31
console.log(error)
_31
if (error) throw error
_31
_31
return new Response(JSON.stringify(data), {
_31
status: 200,
_31
})
_31
} catch (e) {
_31
console.error(e)
_31
return new Response('Internal Server Error', { status: 500 })
_31
}
_31
}

The createSignedUploadUrl method returns a token which we can then use on the client-side to upload the file selected by React Dropzone:

src/app/dashboard/settings/_components/supabase-react-dropzone.tsx

_42
'use client'
_42
_42
import { env } from '@/env'
_42
import { createClient } from '@supabase/supabase-js'
_42
import Image from 'next/image'
_42
import React, { useState } from 'react'
_42
import { useDropzone } from 'react-dropzone'
_42
_42
export default function SupabaseReactDropzone({ userId }: { userId?: string } = {}) {
_42
const supabaseBrowserClient = createClient(
_42
env.NEXT_PUBLIC_SUPABASE_URL,
_42
env.NEXT_PUBLIC_SUPABASE_ANON_KEY
_42
)
_42
const { acceptedFiles, fileRejections, getRootProps, getInputProps } = useDropzone({
_42
maxFiles: 1,
_42
accept: {
_42
'image/jpeg': [],
_42
'image/png': [],
_42
},
_42
onDropAccepted: async (acceptedFiles) => {
_42
setAvatar(null)
_42
console.log(acceptedFiles)
_42
const { path, token }: { path: string; token: string } = await fetch(
_42
'/api/supabase/storage'
_42
).then((res) => res.json())
_42
_42
const { data, error } = await supabaseBrowserClient.storage
_42
.from('avatars')
_42
.uploadToSignedUrl(path, token, acceptedFiles[0])
_42
},
_42
})
_42
_42
return (
_42
<div className="mx-auto mt-4 grid w-full gap-2">
_42
<div {...getRootProps({ className: 'dropzone' })}>
_42
<input {...getInputProps()} />
_42
<p>Drag 'n' drop some files here, or click to select files</p>
_42
<em>(Only *.jpeg and *.png images will be accepted)</em>
_42
</div>
_42
</div>
_42
)
_42
}

Custom Next.js Image loader for Supabase Storage

Supabase Storage also conveniently integrates with the Next.js Image paradigm, by creating a custom loader:

src/lib/supabase-image-loader.ts

_10
import { env } from '@/env'
_10
_10
export default function supabaseLoader({ src, width, quality }) {
_10
return `${env.NEXT_PUBLIC_SUPABASE_URL}/storage/v1/object/public/${src}?width=${width}&quality=${quality || 75}`
_10
}

Now we just need to register the custom loader in the next.config.js file:

next.config.js

_10
images: {
_10
loader: "custom",
_10
loaderFile: "./src/lib/supabase-image-loader.ts",
_10
},

and we can start using the Next.js Image component by simply providing the file path within Supabase Storage:


_10
<Image
_10
alt="Expert image"
_10
className="aspect-square rounded-md object-cover"
_10
src="your-bucket-name/image.png"
_10
height="64"
_10
width="64"
_10
/>

Supabase Vercel Integration for one-click deploys

Supabase also provides a Vercel Integration which makes managing environment variables across branches and deploy previews a breeze. When you connect your Supabase project to your Vercel project, the integration will keep your environment variables in sync.

And when using the Vercel Deploy Button the integration will automatically create a new Supabase project for you, populate the environment variables, and even run the database migration and seed scripts, meaning you're up and running with a full end-to-end application in no time!

Contributing to open source

Both Cal.com and Supabase are on a mission to create open source software, therefore this new platform starter kit is of course also open-source, allowing you to spin up your own marketplace with convenient scheduling in minutes! Of course this also means that you are very welcome to contribute additional features to the starter kit! You can find the repository on GitHub!

Resources

Share this article

Build in a weekend, scale to millions