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 collection 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 collection 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-sdk
Initialize the client:
import { CentraliSDK } from '@centrali-io/centrali-sdk';const centrali = new CentraliSDK({workspaceId: 'my-workspace',clientId: process.env.CLIENT_ID,clientSecret: process.env.CLIENT_SECRET,});
Step 1: Create a Schemaless Collection
Create a collection with schemaDiscoveryMode set to 'schemaless' and no properties:
const structure = await centrali.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 eventawait centrali.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 shapeawait centrali.createRecord('incoming-webhooks', {source: 'stripe',event: 'payment.succeeded',paymentId: 'pi_3RBxK2',amount: 12999,currency: 'usd',metadata: { orderId: 'ORD-4821' },});// Third — yet another shapeawait centrali.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 structureconst all = await centrali.queryRecords('incoming-webhooks');// Filter by a specific fieldconst shopify = await centrali.queryRecords('incoming-webhooks', {'data.source': 'shopify',});// Get a single record by IDconst record = await centrali.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 suggestionsconst 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 collection. 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 centrali = new CentraliSDK({workspaceId: 'my-workspace',clientId: process.env.CLIENT_ID,clientSecret: process.env.CLIENT_SECRET,});// One-time setup: create the schemaless structureasync function setup() {await centrali.structures.create({name: 'Webhook Events',slug: 'webhook-events',schemaDiscoveryMode: 'schemaless',enableVersioning: true,});}// Accept any webhook payloadapp.post('/webhooks/:source', async (req, res) => {const record = await centrali.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.