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
- Check the entry named in the message: replace it with the value returned by `table(...)`.
- For submodules, use a real namespace import (`import * as lib from 'lib'`) — that object is what `isSubmoduleNamespace` detects.
- Move config/constants out of the schema entries object.
- 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
- Only put `table()` return values and namespace imports into the schema entries object.
- Never pass `.rowType` or config objects; double-check each entry after refactors.
- Align bindings versions across workspace packages so structural checks on table objects keep passing.
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
- invalid sequence type
- schema entry ' ' looks like a default import; use `import *…
- exporting something that is not a spacetime export
- Failed to generate table due to validation errors
- HTTP handler ' ' was exported more than once
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)