FAQ & Troubleshooting

Here are some common developer friction points and how to resolve them when working with TableCraft.

Common Errors

"Request failed: 404" (Initialization Error)

This usually means your client is pointing to an endpoint that does not exist or your backend server is not running.

  • Solution: Ensure your baseUrl is correct in the createTableCraftAdapter or createClient setup, and check that your backend server is actually up and listening.

Missing Schema Errors

If the server crashes complaining about missing tables or columns during startup:

  • Solution: Ensure you are passing the actual Drizzle schema objects (like schema.users) to defineTable(), and that your database migrations have been applied.

Table styles missing (Tailwind CSS v4)

If the table renders without proper styling (missing borders, spacing, cursor pointers, or column resize handles):

  • Solution 1: Import the package styles in your main CSS file:

    @import "@tablecraft/table/styles.css";
  • Solution 2: Add the @source directive so Tailwind scans the package for utility classes:

    @source "<path-to-root>/node_modules/@tablecraft/table/src";

    The path must be relative to your CSS file, not the project root. For a CSS file at apps/web/src/index.css, use ../../../node_modules/@tablecraft/table/src. For src/index.css, use ../node_modules/@tablecraft/table/src.

How do I handle Codegen if my server has authentication?

If your TableCraft API requires authentication (e.g., JWT, session cookies, API keys), the tablecraft-codegen CLI will fail to fetch metadata unless you provide credentials.

You can securely pass any required authentication headers using the --header or -H flag:

Using a Bearer Token:

Using Session Cookies: If your backend uses standard cookie-based sessions, you can pass the session cookie directly via the Cookie header:

Using Custom API Keys or Multiple Headers:

Why is my filter not working?

There are a few reasons why a global search or column filter might not be taking effect:

  1. Column Not Included: Ensure the column is explicitly defined in your .search('email', 'firstName') or .filter() configuration block on the backend.

  2. Type Mismatch: Trying to apply a string operation (like contains) on an integer column will fail. Use the appropriate operators for your data types.

  3. Date Formatting: Date strings must be in ISO 8601 format when sent from the client or custom adapter.

  4. Adapter Drop: If you wrote a custom adapter (like GraphQL or Firebase), double-check that your adapter isn't swallowing the params.filters or params.globalSearch payload before passing it to the database query.

Why is my query slow?

Performance issues usually stem from one of two database constraints:

  1. Missing Indexes: Ensure you have database indexes on columns you frequently filter or sort by (e.g., createdAt, tenantId, status).

  2. The COUNT(*) Problem: If you are using standard offset pagination (page, pageSize) and dealing with hundreds of thousands of rows, computing the total row count becomes extremely expensive for SQL databases.

  • Solution: Switch to Cursor Pagination, which bypasses the OFFSET scanning penalty entirely and operates in O(1) time. Alternatively, disable the .count() request on massive datasets.

Client Error Handling

How does the Client SDK handle errors? TableCraft surfaces errors as standard JavaScript Error objects, but attaches additional metadata straight from the backend response.

If you are using the fetch client (or the Axios adapter), you can catch these errors gracefully:

Debugging Tips

  • Inspect Network Payload: Use your browser's Developer Tools (Network tab) to inspect the outgoing request to the engine. Verify that the query parameters (like filters, sort, cursor) match what you expect.

  • Check React Query Keys: If you are using useTableQuery and data isn't updating when state changes, ensure the query key includes all dependencies (which TableCraft handles automatically under the hood, but it's good to verify using the React Query DevTools).


Subqueries

Why do I get a 400 when I sort by a first subquery field?

The 'first' subquery type uses PostgreSQL's row_to_json() function, which returns a JSON object, not a scalar value. SQL databases cannot use a JSON object in an ORDER BY clause — attempting to do so produces a cryptic database error.

TableCraft prevents this by marking 'first' subquery columns as sortable: false and rejecting the request with a FieldError (HTTP 400) before the query is sent:

Use 'count' (integer) or 'exists' (boolean) if you need a sortable subquery field.

Why does my first subquery throw a DialectError on MySQL/SQLite?

'first' mode relies on row_to_json(), which is PostgreSQL-only. If the engine detects a non-PostgreSQL dialect (MySQL, SQLite), it throws a DialectError (HTTP 400) instead of sending a query that will fail at the database level:

Switch to PostgreSQL, or use 'count' / 'exists' which work on all dialects.

Can I filter by a subquery field?

No. All subquery types (count, exists, first) have filterable: false. Subquery expressions are computed per-row in the SELECT clause — they cannot be used in a WHERE clause without wrapping the entire query in a subquery, which TableCraft does not do automatically. Use a join or a backend condition instead.


Cursor Pagination

Why do I get duplicate or skipped rows when using cursor pagination with a multi-column sort?

This happens when the cursor only encodes the primary sort field. If multiple rows share the same primary sort value, the engine cannot tell which ones were already seen.

TableCraft's cursor encodes all sort field values from the last row, and the continuation WHERE uses a lexicographic OR-expansion:

If you are still seeing duplicates, ensure your sort includes a unique tiebreaker (e.g., ?sort=status,id), which guarantees a strict total order across all rows.

What happens if I sort by a computed column and use cursor pagination?

Cursor pagination resolves WHERE conditions against base table columns only. If a sort field resolves exclusively to a SQL expression (e.g., a subquery or computed column), the engine currently includes it in ORDER BY but skips it in the cursor WHERE. This means cursor pagination with computed-only sort fields behaves correctly for ordering but may not produce a perfectly stable continuation on ties.

For stable cursor pagination, always include at least one base column (e.g., id) as a tiebreaker in your sort.


Column Order & System Columns

Why is select always first even though I didn't put it there?

select (the row selection checkbox) and __actions (the actions column) are system columns — they are pinned automatically by the table on every order change:

  • select → always first

  • __actions → always last

This happens regardless of what is stored in localStorage, passed via defaultColumnOrder, or produced by a drag-and-drop reorder. You never need to include them in your defaultColumnOrder array:

Can I put select or __actions in a custom position?

No. Both are intentionally non-movable. select is always first so the checkbox column is never lost in the middle of the table after a reorder or page reload. __actions is always last so the row actions menu stays at a predictable edge position.

If your design requires a different layout (e.g., actions before the data columns), the recommended approach is to pass a custom actions render via the actions prop and also supply a manual columns prop — this gives you full control over column definition order without relying on the automatic system column pinning.

My stored column order from a previous version put __actions in the wrong position. How do I fix it?

The table automatically normalizes any order it loads from localStorage — it strips select and __actions from wherever they appear and re-pins them at first/last. So stale saved orders are corrected automatically on the next page load. No manual cleanup is needed.

How does drag-and-drop column reorder interact with defaultColumnOrder?

  1. On first mount, if no saved order exists in localStorage, defaultColumnOrder is applied (with select/__actions pinned automatically).

  2. When the user drags columns in the View popover, the new order is saved to localStorage and takes precedence over defaultColumnOrder on subsequent loads.

  3. When the user clicks "Reset Column Order", the table reverts to defaultColumnOrder (or the natural definition order if defaultColumnOrder is not set) and saves that back to localStorage.

Can I mark a computed column as non-sortable?

Yes. Pass { sortable: false } as the third argument to .computed():

This prevents users from requesting ?sort=jsonMeta and getting a database error from a non-scalar expression.

Last updated

Was this helpful?