Add Server-Side Rendering to your App

This guide will take your simple client-side app to the next level by showing you how to create a server-rendered page to publish your data to the world.

If you haven't gone through the front-end Quickstart, you might find this guide a bit confusing. If you're looking for a quick reference, you might find this page more helpful!

Creating an agent

For Jazz to access data on the server, we need to create an SSR agent, which is effectively a read-only user which can access public data stored in Jazz.

We can create this user using the createSSRJazzAgent function. In this example, we'll create a new file and export the agent, which allows us to import and use the same agent in multiple pages.

app/jazzSSR.ts
import { createSSRJazzAgent } from "jazz-tools/ssr";

export const jazzSSR = createSSRJazzAgent({
  peer: "wss://cloud.jazz.tools/",
});

Telling Jazz to use the SSR agent

Normally, Jazz expects a logged in user (or an anonymous user) to be accessing data. We can use the enableSSR setting to tell Jazz that this may not be the case, and the data on the page may be being accessed by an agent.

app/components/JazzWrapper.tsx
No content available for tab:

Making your data public

By default, when you create data in Jazz, it's private and only accessible to the account that created it.

However, the SSR agent is credential-less and unauthenticated, so it can only read data which has been made public. Although Jazz allows you to define complex, role-based permissions, here, we'll focus on making the CoValues public.

app/schema.ts
import { co, z } from "jazz-tools";

export const Band = co.map({
  name: z.string(), // Zod primitive type
})
  .withMigration(band => {
    band.$jazz.owner.makePublic()
  });

export const Festival = co.list(Band)
  .withMigration(festival => festival.$jazz.owner.makePublic());

export const JazzFestAccountRoot = co.map({
  myFestival: Festival,
});

export const JazzFestAccount = co
  .account({
    root: JazzFestAccountRoot,
    profile: co.profile(),
  })
  .withMigration((account) => {
    if (!account.$jazz.has('root')) {
      account.$jazz.set('root', {
        myFestival: [],
      });
      account.root?.myFestival?.$jazz.owner.makePublic();
    }
  });

Creating a server-rendered page

Now let's set up a page which will be read by the agent we created earlier, and rendered fully on the server.

app/festival/[festivalId]/page.tsx
No content available for tab:

Note: filter(Boolean) allows us to remove any bands which have resolved to null during the loading process — this is often the case when the SSR agent tries to load a CoValue where the permissions are not set to public.

TypeScript might not recognise that params is a promise. This is a new feature in Next.js 15, which you can read more about here.

Linking to your server-rendered page

The last step is to link to your server-rendered page from your Festival component so that you can find it easily!

app/components/Festival.tsx

Start your app

Let's fire up your app and see if it works!

No content available for tab:

If everything's going according to plan, your app will load with the home page. You can click the link to your server-rendered page to see your data - fully rendered on the server!

Congratulations! 🎉 You've now set up server-side rendering in your React app. You can use this same pattern to render any page on the server.

Not working?

  • Did you add enableSSR to the provider?
  • Did you add loadAs: jazzSSR to Festival.load?
  • Did you add the migrations to make the data public?
Still stuck?Ask for help on Discord!

Next steps