@twentyfourg/tabletop
v0.1.0
Published
A local web GUI for visualizing ElectroDB single-table DynamoDB designs
Keywords
Readme
electrodb-gui

A local web GUI for visualizing and interacting with ElectroDB single-table DynamoDB designs.
Spin up a local server from your Node.js code, point it at your ElectroDB entities, and get a browser-based UI for browsing records, running queries, and performing CRUD operations — without leaving your dev environment.
Screenshots
Data browser — browse and filter records across all your entities and access patterns, with sortable/pinnable/hideable columns, inline filter chips, and a resizable record inspector.

Command palette (⌘K) — quickly jump to any entity or access pattern by name, with keyboard navigation and pattern preview.

Prerequisites
- Node.js 18+
- An ElectroDB
Entityinstance (v3+) - AWS credentials available in your environment (via
AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY/AWS_REGION, or an AWS profile)
Installation
npm install electrodb-guielectrodb and @aws-sdk/client-dynamodb are peer dependencies — install them if you haven't already:
npm install electrodb @aws-sdk/client-dynamodbQuick Start
import { startGui } from "electrodb-gui";
import { UserEntity } from "./models/user";
const server = await startGui([
{ entity: UserEntity, browsePattern: "primary" },
]);
// Open http://localhost:4321 in your browser
// Call server.stop() when you're doneFull Example
Here's a realistic example with a multi-index ElectroDB entity:
import { Entity } from "electrodb";
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { startGui } from "electrodb-gui";
const client = new DynamoDBClient({ region: "us-east-1" });
const MessageEntity = new Entity(
{
model: {
entity: "message",
version: "1",
service: "chat",
},
attributes: {
tenantId: { type: "string", required: true },
channelId: { type: "string", required: true },
messageId: { type: "string", required: true },
content: { type: "string" },
createdAt: { type: "string", default: () => new Date().toISOString() },
},
indexes: {
primary: {
pk: { field: "pk", composite: ["tenantId"] },
sk: { field: "sk", composite: ["channelId", "messageId"] },
},
byChannel: {
index: "gsi1pk-gsi1sk-index",
pk: { field: "gsi1pk", composite: ["channelId"] },
sk: { field: "gsi1sk", composite: ["createdAt"] },
},
},
},
{ client, table: "my-app-table" },
);
const server = await startGui(
[{ entity: MessageEntity, browsePattern: "byChannel" }],
{ port: 4321 },
);
console.log("GUI running at http://localhost:4321");
// Shut down gracefully on Ctrl+C
process.on("SIGINT", async () => {
await server.stop();
process.exit(0);
});API Reference
startGui(registrations, options?)
Starts the GUI server and returns a handle to stop it.
const server = await startGui(registrations, options);| Parameter | Type | Description |
| --------------- | --------------------- | ------------------------------------------- |
| registrations | ModelRegistration[] | One or more entity registrations (required) |
| options | StartGuiOptions | Optional server configuration |
Returns Promise<GuiServer>.
ModelRegistration
interface ModelRegistration {
entity: Entity; // An ElectroDB Entity instance
browsePattern?: string; // Index name to use for the default browse/list view (optional)
}browsePattern must exactly match a key in entity.schema.indexes when provided. This index powers the main record list in the GUI. Omit it for large datasets where a full-table browse is impractical — the GUI will still show all access patterns in the sidebar and let you run targeted queries without auto-loading data on entity selection.
Example:
{ entity: MessageEntity, browsePattern: 'byChannel' }
// browsePattern 'byChannel' must exist in MessageEntity.schema.indexesTip: browse an existing table using __edb_e__
ElectroDB writes a hidden __edb_e__ field (set to the entity name) on every item it creates. If your table already has data you can point a GSI at that field to get a zero-cost "browse all records for this model" index — no data migration needed.
1. Create the GSI in DynamoDB (Terraform example):
global_secondary_index {
name = "byEntityType-index"
hash_key = "__edb_e__"
range_key = "createdAt" # Or udpatedAt
projection_type = "ALL"
}2. Add the attribute and index to your entity:
import { Entity } from "electrodb";
export const CustomerEntity = new Entity(
{
model: { entity: "customer", version: "1", service: "shop" },
attributes: {
tenantId: { type: "string", required: true },
customerId: { type: "string", required: true },
createdAt: { type: "string", default: () => new Date().toISOString() },
// Expose the field ElectroDB already writes on every item.
// hidden + readOnly keeps it out of your application types.
__edb_e__: {
type: "string",
hidden: true,
readOnly: true,
default: "customer", // must match model.entity
},
},
indexes: {
primary: {
pk: { field: "pk", composite: ["tenantId"] },
sk: { field: "sk", composite: ["customerId"] },
},
// GSI backed by __edb_e__ — all existing items already have this field
byModel: {
index: "byEntityType-index",
pk: { field: "__edb_e__", composite: ["__edb_e__"] },
sk: { field: "createdAt", composite: ["createdAt"] },
},
},
},
{ client, table: TABLE_NAME },
);3. Register byModel as the browse pattern:
const server = await startGui([
{ entity: CustomerEntity, browsePattern: "byModel" },
]);Because __edb_e__ is auto-filled by ElectroDB, the GUI can browse the entire entity without requiring the user to enter a partition key value.
StartGuiOptions
interface StartGuiOptions {
port?: number; // Default: 4321
}GuiServer
interface GuiServer {
stop(): Promise<void>; // Gracefully shuts down the HTTP server
}Error Cases
| Situation | Error |
| ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| registrations is empty | "at least one ModelRegistration is required" |
| browsePattern doesn't match any index | Entity "message": browsePattern "byFoo" does not match any defined index. Available indexes: primary, byChannel |
| Two entities share the same model.entity name | "duplicate entity name" |
| Port already in use | Logs a message and exits with code 1 |
Dark Mode
The GUI supports light and dark mode. Toggle it with the button in the top-right corner. Your preference is persisted to localStorage.
Notes
reactandreact-domare bundled inside the package — you don't need to install them separately.- The GUI is read/write. It supports creating, updating, and deleting records directly from the browser.
- No data leaves your machine — all DynamoDB calls are made server-side using your local AWS credentials.
