clockworklabs/SpacetimeDB · error · TypeError

schema entry ' ' must be a table or a submodule namespace…

Error message

schema entry '${accName}' must be a table or a submodule namespace object

What it means

`schema()` accepts an entries object whose values must each be either a table definition (created by `table()`) or a submodule namespace object (detected by `isSubmoduleNamespace`). Anything else — numbers, strings, plain objects, arrays, class instances, the row-type object — fails `isUntypedTableSchema` and throws `schema entry '<name>' must be a table or a submodule namespace object`, naming the offending key.

Solutions

  1. Check the entry named in the message: replace it with the value returned by `table(...)`.
  2. For submodules, use a real namespace import (`import * as lib from 'lib'`) — that object is what `isSubmoduleNamespace` detects.
  3. Move config/constants out of the schema entries object.
  4. If the value came from a dependency, align the dependency's bindings version with the app's (`pnpm` dedupe / matching `@clockworklabs/spacetime-sdk` versions).

Example fix

// before
export default schema({
  users: usersTable,
  settings: { tickRate: 5 }, // plain object
});

// after
export default schema({
  users: usersTable,
  settingsTable, // a real table() handle
});
Defensive patterns

Strategy: validation

Validate before calling

// Validate entries are tables or namespace objects before schema().
function assertSchemaEntries(entries: Record<string, unknown>) {
  for (const [name, entry] of Object.entries(entries)) {
    const looksLikeTable = typeof entry === 'object' && entry !== null && 'tableDef' in entry;
    const looksLikeNamespace = typeof entry === 'object' && entry !== null && 'default' in entry;
    if (!looksLikeTable && !looksLikeNamespace) {
      throw new Error(`schema entry '${name}' is neither a table() handle nor a namespace import`);
    }
  }
}

Type guard

function isTableHandle(x: unknown): boolean {
  return typeof x === 'object' && x !== null && typeof (x as any).tableDef === 'function';
}

Prevention

When it happens

Trigger: `schema({ users: 42 })`, `schema({ row: usersRowTypeObj })` (passing the row type instead of the table), `schema({ cfg: { tick: 1 } })` (config object), or a namespace import that resolved to a non-module value; a schema entry whose table came from a different, incompatible bindings version.

Common situations: Passing `.rowType` or a raw row builder instead of the table handle; leaving a leftover config entry inside the schema object; version skew between a library compiled against an older bindings package so its table objects fail the structural check.

Related errors


AI-assisted analysis of clockworklabs/SpacetimeDB@eddf9f5014 (2026-08-20). Data as JSON: /api/errors/2a08e5f63e7f566c. Report an issue: GitHub.

Appendix: source

Thrown at crates/bindings-typescript/src/server/schema.ts:868

    }

    const tableSchemas: Record<string, UntypedTableDef> = {};
    for (const [accName, entry] of Object.entries(entries)) {
      if (entry instanceof Schema) {
        throw new TypeError(
          `schema entry '${accName}' looks like a default import; use \`import * as ${accName} from '...'\` so the submodule can see the library's named reducer exports.`
        );
      }
      if (isSubmoduleNamespace(entry)) {
        const { rawDef, dispatch } =
          entry.default.buildSubmoduleDispatch(entry);
        dispatch.namespace = accName;
        ctx.addSubmodule({ namespace: accName, module: rawDef });
        ctx.submoduleDispatchInfos.push(dispatch);
        continue;
      }
      if (!isUntypedTableSchema(entry)) {
        throw new TypeError(
          `schema entry '${accName}' must be a table or a submodule namespace object`
        );
      }

      const table = entry;
      const tableDef = table.tableDef(ctx, accName);
      tableSchemas[accName] = tableToSchema(accName, table, tableDef);
      const tableSourceNames = ctx.tableSourceNames.get(table);
      if (tableSourceNames === undefined) {
        ctx.tableSourceNames.set(table, [tableDef.sourceName]);
      } else {
        tableSourceNames.push(tableDef.sourceName);
      }
      ctx.moduleDef.tables.push(tableDef);
      if (table.schedule) {
        ctx.pendingSchedules.push({
          table,
          tableName: tableDef.sourceName,

View on GitHub (pinned to eddf9f5014)