Skip to content

Next.js Quick Start

Build a Next.js app powered by Centrali in 5 minutes. You'll create an API route that fetches data from Centrali and a page that displays it.

Prerequisites

  • A Centrali account with a workspace (Account Setup)
  • Service account credentials (client ID and secret), added to the workspace_administrators or workspace_developers group so they may create a collection
  • Node.js 20.9+ (required by current Next.js)

Which credential?

This guide uses a service account, read only on the server. A publishable key cannot create a collection — the platform rejects every non-GET /collections request made with one. Never give a service account secret a NEXT_PUBLIC_ prefix: that inlines it into the browser bundle.

1. Create a Next.js App

npx create-next-app@latest my-centrali-app --typescript --app
cd my-centrali-app

2. Install the SDK

npm install @centrali-io/centrali-sdk

3. Set Up Environment Variables

Create a .env.local file:

CENTRALI_CLIENT_ID=sa_your_client_id
CENTRALI_CLIENT_SECRET=sk_your_client_secret
CENTRALI_WORKSPACE=your-workspace-slug

Note

CENTRALI_WORKSPACE holds your workspace slug, not its display name. The create-centrali-app templates use this same variable name.

4. Create the Centrali Client

Create lib/centrali.ts:

import { CentraliSDK } from '@centrali-io/centrali-sdk';

export const centrali = new CentraliSDK({
  // Use the root domain — the SDK derives api.centrali.io itself.
  baseUrl: 'https://centrali.io',
  workspaceId: process.env.CENTRALI_WORKSPACE!, // current SDK option name; pass the workspace slug
  clientId: process.env.CENTRALI_CLIENT_ID!,
  clientSecret: process.env.CENTRALI_CLIENT_SECRET!
});

5. Create the Collection

A new workspace has no collections, so create the one this guide uses before querying it. Save this as scripts/setup.mjs and run it once with node scripts/setup.mjs:

import 'dotenv/config';
import { CentraliSDK } from '@centrali-io/centrali-sdk';

const centrali = new CentraliSDK({
  baseUrl: 'https://centrali.io',
  workspaceId: process.env.CENTRALI_WORKSPACE,
  clientId: process.env.CENTRALI_CLIENT_ID,
  clientSecret: process.env.CENTRALI_CLIENT_SECRET
});

const existing = await centrali.collections.list();
if ((existing.data ?? []).some((c) => c.recordSlug === 'products')) {
  console.log('Collection "products" already exists');
} else {
  await centrali.collections.create({
    name: 'Products',
    recordSlug: 'products',
    properties: [
      { name: 'name', type: 'string', required: true },
      { name: 'price', type: 'number' },
      { name: 'inStock', type: 'boolean' }
    ],
    schemaDiscoveryMode: 'strict'
  });
  console.log('Created collection "products"');
}

If this returns 403

The credentials authenticated but are not permitted to create collections. Add the service account to the workspace_administrators or workspace_developers group. Obtaining a token does not by itself grant access to a resource.

6. Create an API Route

Create app/api/products/route.ts:

import { centrali } from '@/lib/centrali';
import { NextResponse } from 'next/server';

export async function GET() {
  const products = await centrali.records.list('products', {
    sort: '-createdAt',
    pageSize: 20
  });

  return NextResponse.json(products);
}

// `records.list()` is the GET adapter for simple, URL-encodable queries. For
// nested boolean conditions, field selection or full-text search, use the
// canonical form: records.query('products', { resource: 'products',
// where: { 'data.inStock': { eq: true } }, sort: [{ field: 'createdAt',
// direction: 'desc' }], page: { limit: 20 } }).

export async function POST(request: Request) {
  const body = await request.json();

  const product = await centrali.createRecord('products', {
    name: body.name,
    price: body.price,
    inStock: true
  });

  return NextResponse.json(product, { status: 201 });
}

7. Create a Page

Replace app/page.tsx:

async function getProducts() {
  const res = await fetch(`${process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000'}/api/products`, {
    cache: 'no-store'
  });
  return res.json();
}

export default async function Home() {
  const products = await getProducts();

  return (
    <main style={{ padding: '2rem', fontFamily: 'system-ui' }}>
      <h1>Products</h1>
      {products.data?.length > 0 ? (
        <ul>
          {products.data.map((product: any) => (
            <li key={product.id}>
              <strong>{product.data.name}</strong> — ${product.data.price}
            </li>
          ))}
        </ul>
      ) : (
        <p>No products yet. Create some using the API!</p>
      )}
    </main>
  );
}

8. Run It

npm run dev

Visit http://localhost:3000 to see your page. Create a product:

curl -X POST http://localhost:3000/api/products \
  -H "Content-Type: application/json" \
  -d '{"name": "Wireless Headphones", "price": 99.99}'

Refresh the page to see it appear.

This route has no access control

/api/products reads and writes your workspace using service account credentials and does not check who is calling it. That is fine locally. Before deploying it publicly, add your own authentication and authorization, and use a separate service account and workspace for production. A working deploy is not evidence that access control is correct.

What's Next?