Most platforms force you to define every field before you can store a single record. Column names, types, constraints — all upfront. That's fine when you know your data model inside out. But what about when you don't?
Maybe you're prototyping a new feature. Maybe you're ingesting data from a third-party webhook and the payload keeps changing. Maybe you just want to move fast and figure out the structure later.
That's what schemaless mode is for.
What Is Schemaless Mode?
Every structure in Centrali has a schema discovery mode. The default is strict — you define properties, and records are validated against them. Schemaless mode flips that on its head: you create a structure with no properties at all, and start writing data immediately. Centrali accepts whatever you send.
Behind the scenes, the system buffers your records and runs inference to learn the shape of your data. It then surfaces schema suggestions — proposed properties with inferred types — that you can accept, reject, or ignore.
Three modes are available:
| Mode | What happens |
|---|---|
| strict | Full validation. Extra fields are rejected. |
| schemaless | No validation. Any JSON is accepted. |
| auto-evolving | Known fields are validated, unknown fields are accepted. |
This post focuses on schemaless — the "just let me write data" mode.
Getting Started with the SDK
Install the SDK:
npm install @centrali-io/centrali-sdkInitialize the client:
import { CentraliSDK } from '@centrali-io/centrali-sdk';
const client = new CentraliSDK({
workspaceId: 'my-workspace',
clientId: process.env.CLIENT_ID,
clientSecret: process.env.CLIENT_SECRET,
});Step 1: Create a Schemaless Structure
Create a structure with schemaDiscoveryMode set to 'schemaless' and no properties:
const structure = await client.structures.create({
name: 'Incoming Webhooks',
slug: 'incoming-webhooks',
schemaDiscoveryMode: 'schemaless',
});That's it. No properties array. The structure is ready to accept data.
Step 2: Start Pushing Records
Now write records with whatever shape you need. No schema to worry about:
// First webhook payload — an order event
await client.createRecord('incoming-webhooks', {
source: 'shopify',
event: 'order.created',
orderId: 'ORD-4821',
customer: { name: 'Alice', email: 'alice@example.com' },
total: 129.99,
currency: 'USD',
items: [
{ sku: 'WIDGET-A', quantity: 2, price: 49.99 },
{ sku: 'GADGET-B', quantity: 1, price: 30.01 },
],
});
// Second webhook — a completely different shape
await client.createRecord('incoming-webhooks', {
source: 'stripe',
event: 'payment.succeeded',
paymentId: 'pi_3RBxK2',
amount: 12999,
currency: 'usd',
metadata: { orderId: 'ORD-4821' },
});
// Third — yet another shape
await client.createRecord('incoming-webhooks', {
source: 'github',
event: 'push',
repo: 'acme/api',
branch: 'main',
commits: 3,
author: 'bob',
});All three records are accepted and stored. No errors, no validation failures — even though every record has a different shape.
Step 3: Query Your Data
Records are fully queryable from the moment they're created:
// Get all records from the structure
const all = await client.queryRecords('incoming-webhooks');
// Filter by a specific field
const shopify = await client.queryRecords('incoming-webhooks', {
'data.source': 'shopify',
});
// Get a single record by ID
const record = await client.getRecord('incoming-webhooks', recordId);The data is stored in PostgreSQL JSONB, so queries against nested fields work out of the box.
What Happens Behind the Scenes
While you're writing data, Centrali is quietly doing work in the background:
- Buffering — Each record is added to a schema discovery buffer.
- Batching — Once enough records accumulate (default: 10), inference kicks in.
- Analysis — The system examines the buffered records and detects field names, types, and patterns.
- Suggestions — It creates schema suggestions: proposed properties like
source (string),total (number), orcustomer (object).
You can view and act on these suggestions through the API:
// List pending suggestions
const suggestions = await fetch(
'/structures/incoming-webhooks/schema-suggestions',
{ headers: { Authorization: `Bearer ${token}` } }
).then(r => r.json());
// Accept a suggestion (adds the property to the structure)
await fetch(
`/structures/incoming-webhooks/schema-suggestions/${suggestionId}/accept`,
{ method: 'POST', headers: { Authorization: `Bearer ${token}` } }
);Once you accept suggestions, those properties become part of the structure. If you later switch the mode to strict, new records will be validated against the accepted schema.
Operational Limits
Schemaless mode isn't a free-for-all. Centrali enforces sensible limits to prevent abuse:
| Limit | Default |
|---|---|
| Max keys per record | 100 |
| Max nesting depth | 3 levels |
| Max string length sampled | 1,000 characters |
These are configurable per structure through the schema discovery settings endpoint.
When to Use Schemaless Mode
Schemaless mode is ideal when:
- Prototyping — You're exploring a data model and don't want to commit to a schema yet.
- Ingesting external data — Webhooks, APIs, or imports where the shape varies or evolves.
- Migration staging — You're moving data from another system and want to land it first, then formalize the schema.
- Event collection — Capturing events from multiple sources with different payloads.
When your data model stabilizes, switch to auto-evolving (validates known fields, accepts new ones) or strict (full validation). The transition is seamless — existing records aren't affected.
Full Example: Webhook Ingestion Pipeline
Here's a complete example — a webhook endpoint that stores incoming payloads in a schemaless structure:
import { CentraliSDK } from '@centrali-io/centrali-sdk';
import express from 'express';
const app = express();
app.use(express.json());
const client = new CentraliSDK({
workspaceId: 'my-workspace',
clientId: process.env.CLIENT_ID,
clientSecret: process.env.CLIENT_SECRET,
});
// One-time setup: create the schemaless structure
async function setup() {
await client.structures.create({
name: 'Webhook Events',
slug: 'webhook-events',
schemaDiscoveryMode: 'schemaless',
enableVersioning: true,
});
}
// Accept any webhook payload
app.post('/webhooks/:source', async (req, res) => {
const record = await client.createRecord('webhook-events', {
source: req.params.source,
receivedAt: new Date().toISOString(),
headers: {
contentType: req.headers['content-type'],
userAgent: req.headers['user-agent'],
},
...req.body,
});
res.status(201).json({ id: record.data.id });
});
app.listen(3001);After a few days of webhook traffic, check the schema suggestions. Accept the ones that make sense, and you'll have a fully typed structure built from real data — not guesswork.
What's Next
Schemaless mode is one of three schema discovery modes in Centrali. In a future post, we'll cover auto-evolving mode — the middle ground that validates what it knows and learns from what it doesn't.
For now, try schemaless mode the next time you're starting a new project or integrating an external data source. Skip the schema, start building, and let Centrali figure out the rest.