npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@twentyfourg/tabletop

v0.1.0

Published

A local web GUI for visualizing ElectroDB single-table DynamoDB designs

Readme

electrodb-gui

Tabletop

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.

Data browser

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

Command palette


Prerequisites

  • Node.js 18+
  • An ElectroDB Entity instance (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-gui

electrodb and @aws-sdk/client-dynamodb are peer dependencies — install them if you haven't already:

npm install electrodb @aws-sdk/client-dynamodb

Quick 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 done

Full 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.indexes

Tip: 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

  • react and react-dom are 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.