Schema-First Approach with ThingsDB

Published: (December 12, 2025 at 01:45 AM EST)
4 min read
Source: Dev.to

Source: Dev.to

Setting the Stage: The Blank Slate

We begin by creating a fresh collection for our application.

new_collection('OrderApp');

Understanding Typed Collections (The Core Defense)

The first step toward protection is establishing a schema. By default, a new ThingsDB collection starts with an empty, unrestricted object (a basic thing) at its root. Any user with write access can add anything to it.

We lock this down by creating a type, App, to serve as the structured blueprint for the collection’s entire existence.

// 1. Create a type for the root of my collection
new_type('App');

// 2. Convert the collection root to the 'App' type
.to_type('App');

Why this matters: The collection is now restricted by the App type. No data can be added or modified unless the modification conforms to changes made to the App type definition. This is your primary schema defense.

Defining the Data Model

Next, we define the structure of the data we want to store—the Order object—and where to keep it within the App root.

// Create an enumerator for consistent order status values
set_enum('OrderStatus', {
    OPEN: 'Open',
    PENDING: 'Pending',
    CLOSED: 'Closed',
});

// Define the Order structure
set_type('Order', {
    id: '#',               // Return ID as `id` when wrapped
    name: 'str',
    price: 'float',
    status: 'OrderStatus', // Enforcing status consistency
    created: 'datetime',
});

// Update the App type: Add a property 'orders' as an array of Order objects
mod_type('App', 'add', 'orders', '[Order]');

The mod_type(..) call automatically creates the orders property as an empty list on the collection root, ready to hold our data.

Establishing an Event Channel

For a modern microservice or component‑based application, polling for data changes is inefficient. We set up an event channel using a Room to enable real‑time communication.

// Add a room property to the App type for event handling
mod_type('App', 'add', 'order_events', 'room');

// For easy identification, we give the room a fixed name
.order_events.set_name('api_order_events');

Benefit: Any component can now join this room and instantly react to events like new-order or change-order-status without needing to constantly query the collection.

Encapsulating Logic with Procedures (The API Layer)

To guarantee that only valid changes are made to the collection, we expose all actions only through Procedures. We prefix these procedures with api_ for easy security whitelisting later.

// 1. Add a new order (with validation on input types)
new_procedure('api_order_add', |name, price| {
    order = Order{
        name:,
        price:,
    };
    .orders.push(order);

    // Emit the 'new-order' event for all listeners
    .order_events.emit('new-order', order.wrap());
    nil;
});

// 2. Change order status
new_procedure('api_order_set_status', |order_id, status| {
    // Input validation: ensures the new status is a valid enum value
    new_status = OrderStatus{||status.upper()};

    // Access and update the specific Order object
    Order(order_id).status = new_status;

    // Emit the 'change-order-status' event
    .order_events.emit('change-order-status', order_id, new_status.value());
    nil;
});

// 3. Retrieve orders by status
new_procedure('api_orders_by_status', |status| {
    order_status = OrderStatus{||status.upper()};
    .orders.filter(|order| order.status == order_status).map_wrap();
});

Strict Security: The Locked‑Down API User

The final and most crucial step is to create a user that can only execute these specific procedures. This prevents any external service from bypassing your defined business logic.

// Create a new user for microservice access
new_user('api');

// Grant access to the collection:
// CHANGE (to modify data), JOIN (to listen to events), RUN (to execute procedures)
grant('//OrderApi', 'api', CHANGE|JOIN|RUN);

// Whitelist: This is the security lock.
// The user can only execute procedures and join rooms starting with 'api_'
whitelist_add('api', 'procedures', /^api_.*/);
whitelist_add('api', 'rooms', /^api_.*/);

// Generate and return the authentication token
new_token('api');

With this setup, the api user is completely restricted. It cannot arbitrarily delete the collection, change its structure, or modify data without going through the validated input and logic defined in your api_ procedures. Your collection is now truly protected by code!

In the next post we will switch gears to the client side, demonstrating how a microservice uses the generated token to connect to the OrderApp collection, perform the authenticated api_ queries, and listen to real‑time event changes emitted through the api_order_events room.

Back to Blog

Related posts

Read more »