# Troubleshooting

Common issues and their solutions. For cloud compiler specific issues, see [Cloud Compiler Troubleshooting](/tooling/cloud-compiler/troubleshooting).

## Compilation Issues

### "Feature must use defineTable()"

Your table file is missing the default export:

{/* doc-compile: skip — wrong/right contrast pair in one fence; the first half is deliberately the broken shape this section names, and both halves declare the same table. */}
```typescript
// Wrong — no default export
export const applications = sqliteTable("applications", { /* columns */ });

// Correct — add defineTable default export
export const applications = sqliteTable("applications", { /* columns */ });
export default defineTable(applications, { /* firewall, guards, crud … */ });
```

Adding the default export also makes this a *feature table*, which means the
managed audit columns must be declared in the columns object. A raw Drizzle
table spells them out literally — see
[Schema](/define/schema) for the canonical lines, or run
`quickback migrate visible-columns` to write them for you.

### "File exports multiple tables with defineTable"

A single file can export multiple Drizzle tables, but only ONE can have a `defineTable()` default export. Split tables into separate files:

```
quickback/features/applications/
├── applications.ts           # defineTable(applications, { ... })
└── candidates.ts             # defineTable(candidates, { ... })
```

### Missing fields in generated API

If a field exists in your schema but doesn't appear in API responses:

1. **Guards:** Check that the field is in `createable` (for POST) or `updatable` (for PATCH). Fields not listed are silently stripped.
2. **Views:** If using views, the field must be in the view's `fields` array.
3. **Masking:** The field may be present but masked (showing `[REDACTED]` or `***`). Check your masking config.

### Scope column not auto-detected

The firewall auto-detects the org column across six accepted spellings —
`organizationId`, `organisationId`, `orgId`, `organization`, `organisation`, `org` —
and `ownerId` as the owner column. If your column uses any other name, declare the
firewall explicitly:

```typescript
firewall: [
  { field: 'tenantId', equals: 'ctx.activeOrgId' },
]
```

Or use `q.scope()` and override the SQL name:

```typescript
columns: {
  tenantId: q.scope('organization', { sqlName: 'tenant_id' }),
}
```

### "userId" is not the owner column

`ownerId` is the owner column: the compiler firewalls reads with
`WHERE ownerId = ctx.userId` and auto-stamps the column on insert. `userId` is
**not** auto-detected on feature tables — it is a Better Auth convention on Better
Auth's own tables (`session`, `account`, `member`, `passkey`), and on a feature
table it usually means *a user this row references* (member, contact, invitee)
rather than *the row's owner*. If `userId` is the only candidate column on the
table the compiler raises an error; alongside a detected isolation column it
raises a warning.

Two escapes: rename the column to `ownerId`, or declare it explicitly.

```typescript
firewall: { owner: { column: "userId" } }
```

See [Firewall](/define/firewall) for the full rule.

## Migration Issues

### Columns persist after removal from schema

Quickback (via Drizzle) does **not** generate DROP COLUMN migrations. If you remove a column from your schema, the database column will remain as an orphaned column. SQLite handles this gracefully — the column won't appear in API responses since Drizzle only selects defined columns.

To clean up, you'd need to manually create a migration.

### Drizzle rename prompts in CI

If compilation fails with `drizzle-kit requested interactive rename input`, add explicit rename hints in your config. See [Drizzle rename prompts](/tooling/cloud-compiler/troubleshooting#drizzle-rename-prompts-in-ciheadless-compile).

### Cloudflare D1 destructive migrations

Quickback now adds a D1-specific safety pass around Drizzle-generated migrations for the `cloudflare-d1` provider:

1. **Better Auth cleanup** for retired managed tables is stripped instead of shipped as raw `DROP TABLE` SQL.
2. **Drop-only cleanup migrations** are reordered child-first when foreign keys require it.
3. **All D1 migration directories** generated by the compiler are replayed against SQLite during compile, so a D1-breaking migration fails before deploy.

If `quickback compile` now fails with a D1 replay error, treat that as a real migration problem instead of a deploy-time surprise. The generated SQL needs manual intervention or a schema change that Drizzle can express more safely.

## Runtime Issues

### 401 on all requests

1. **Missing auth:** Ensure your request includes `Authorization: Bearer <token>` or the session cookie
2. **Expired session:** Sessions last 7 days by default. Re-authenticate.
3. **Wrong auth URL:** Check that `BETTER_AUTH_URL` matches your deployment URL

### 403 Forbidden

1. **Wrong role:** Check the `access` config for the endpoint — your role may not be listed
2. **Wrong org:** You may be in a different organization than the record's `organizationId`
3. **Firewall miss:** A firewall miss returns **403** by default (`firewallErrorMode: 'reveal'`). Set `firewallErrorMode: 'hide'` on the resource to return an opaque 404 instead, so cross-tenant probes can't confirm a row exists.

### 400 Bad Request on create/update

1. **Guard violation:** You're sending a field not in `createable` or `updatable`. Check the error's `details.fields`.
2. **Missing required field:** A `.notNull()` column without a default is missing from your request body.
3. **Immutable field:** You're trying to update a field marked as `immutable`.

### Records missing from list

Records may be filtered by:
1. **Firewall:** Only records matching your organization (and optionally owner) are returned
2. **Soft delete:** Soft-deleted records are automatically excluded
3. **Access conditions:** Record-level conditions may filter results

### Recovering a previous `src/` tree

The CLI archives the previous `src/` tree to `quickback/.archives/src-<ISO-timestamp>/` before each compile (most recent 3 retained). If a generator regression silently dropped a file you depended on, restore it from the latest archive:

```bash
ls quickback/.archives/
# src-2026-04-27T16-31-02-014/   ← latest
# src-2026-04-27T16-29-44-882/
# src-2026-04-27T16-12-19-301/

# Restore the whole tree (back-out a bad compile):
rm -rf src
cp -R quickback/.archives/src-2026-04-27T16-31-02-014 src

# …or grep for what disappeared:
grep -r "missingExportName" quickback/.archives/src-2026-04-27T16-31-02-014/
```

Anything in `src/` not produced by the latest compile is removed on every compile — see [CLI → Compile Definitions](/tooling/cli#compile-definitions) for the full contract.

## See Also

- [Cloud Compiler Troubleshooting](/tooling/cloud-compiler/troubleshooting) — CLI and authentication issues
- [Errors](/api/errors) — Complete error code reference
- [Firewall](/define/firewall) — Data isolation configuration
- [Guards](/define/guards) — Field modification rules
