Prompt file imported from aineahmarabi/Point-Of-Sale (
.github/prompts/convex-table.prompt.md). Copyright stays with the author.
Scaffold a complete Convex table from the specification below.
Input spec: $input
Parsing Rules
Extract from the spec:
- Table name — the word after
table(e.g.table categories→categories) - Fields — space-separated field names (e.g.
name description price). If no fields are given, infer sensible defaults from the table name. - Folder — the path after
in(e.g.in data→convex/data/). If omitted, place the functions file directly inconvex/.
Field name → Convex validator inference
Infer the validator from the field name using these rules in order:
| Pattern | Validator | Optional? |
|---|---|---|
id, *Id, *_id (e.g. userId, categoryId) |
v.id("<inferredTable>") — infer table name by stripping Id suffix and pluralising |
No |
price, amount, total, cost, balance, rate, weight, lat, lng, latitude, longitude |
v.float64() |
No |
count, quantity, order, position, rank, age, year, month, day |
v.int64() |
No |
is*, has*, can*, active, enabled, published, verified, deleted |
v.boolean() |
No |
*At, *Date, createdAt, updatedAt, deletedAt |
v.number() (Unix ms timestamp) |
*At other than createdAt → v.optional(v.number()) |
status, role, type, kind, tier, state |
v.string() (use a v.union(v.literal(...)) if you can infer obvious values from the table context) |
No |
url, imageUrl, avatarUrl, *Url |
v.string() |
Yes — v.optional(v.string()) |
description, bio, notes, body, content, summary, excerpt |
v.string() |
Yes — v.optional(v.string()) |
phone, address, website, *Link |
v.string() |
Yes — v.optional(v.string()) |
*s plural nouns for collections (e.g. images, tags, photos, attachments, urls, files) |
v.array(v.string()) |
No |
| Everything else | v.string() |
No |
Never include slug in the field list — it is auto-added whenever name is present.
Step 1 — Update validators.ts
File: packages/backend/convex/validators.ts
- Read the file first to understand existing entries.
- If the table has a
status,role,type, or similar enum field, add aconstenum array in the// ENUMSsection (e.g.export const categoryStatus = ["active", "inactive"] as const;). Usev.union(...enumArray.map((e) => v.literal(e)))in the table shape so the enum is reusable by frontend code viaimport { categoryStatus } from "@repo/backend/validators". - Add a new exported
const <tableName> = { ... }object in the// TABLESsection usingv.*validators fromconvex/values. - If the fields include a
namefield, also include aslug: v.string()— it will be auto-generated fromnameat insert/update time. - Append the new table name to the
export default { ... }object at the bottom.
Follow the exact style of the existing entries:
// ENUMS
export const categoryStatus = ["active", "inactive"] as const;
// TABLES
export const categories = {
name: v.string(),
slug: v.string(),
description: v.optional(v.string()),
status: v.union(...categoryStatus.map((e) => v.literal(e))),
};
Step 2 — Update schema.ts
File: packages/backend/convex/schema.ts
- Read the file first.
- Import the new shape from
./validators. - Register a new
defineTable(<tableName>)entry insidedefineSchema({...}). - Add indexes:
- If there is a
slugfield:.index("by_slug", ["slug"]). - If there is a
statusfield:.index("by_status", ["status"]). - If there is a foreign-key
idfield (e.g.categoryId):.index("by_<fieldName>", ["<fieldName>"]). - If there is a
namefield: add a.searchIndex("by_name", { searchField: "name", filterFields: [...other string/literal fields] }). - Only add indexes that are useful given the actual fields.
- If there is a
Step 3 — Create the functions file
Determine the output path:
- With folder specified (
in data):packages/backend/convex/data/<tableName>.ts - Without folder:
packages/backend/convex/<tableName>.ts - If the folder already exists, add the file inside it. If not, create it.
The file should export five functions (list, count, create, update, remove) following the patterns in packages/backend/convex/data/samples.ts.
All mutations use assertPermission from packages/backend/convex/auth.helpers.ts for permission enforcement. The permission format is "<tableName>:<action>" (e.g. "samples:create", "samples:update", "samples:remove").
list — paginated public query
- Accept
paginationOpts, plus optionalsearch(string) and filter args matching any indexed/searchable fields. - When
searchis provided, use.withSearchIndex(...). When a filter likestatusis provided, use.withIndex(...). Otherwise fall back to an unindexed.query(). - If the table has an
imagestorage field, resolve URLs viactx.storage.getUrl()and merge them into the returned page.
import { paginationOptsValidator } from "convex/server";
import { query, mutation } from "../_generated/server"; // adjust relative path
import { Id } from "../_generated/dataModel"; // only if table has image storage field
import { slugify } from "@repo/lib/utils"; // only if table has name/slug
import { <tableName> } from "../validators";
import { v, ConvexError } from "convex/values";
import { assertPermission } from "../auth.helpers"; // adjust relative path
const { slug: _slug, ...<tableName>Args } = <tableName>; // only if table has slug field
export const list = query({ args: { paginationOpts: paginationOptsValidator, search: v.optional(v.string()), status: v.optional(.status), // if status field exists }, handler: async (ctx, args) => { // branch on search → withSearchIndex, filter → withIndex, else plain query // then .paginate(args.paginationOpts) }, });
### `count` — public query
- Accept the same filter args as `list` (minus `paginationOpts`).
- Use the same index/search branching as `list`, but `.collect()` and return `.length`.
- This is the **only** place `.collect()` is acceptable.
```ts
export const count = query({
args: {
search: v.optional(v.string()),
status: v.optional(<tableName>.status),
},
handler: async (ctx, args) => {
// same branching as list, .collect(), return .length
},
});
create — mutation
- If a
slugfield exists, importslugifyfrom@repo/lib/utilsand generateslug = slugify(name)— do NOT acceptslugas an arg. - Destructure the slug out of the table shape at module level:
const { slug: _slug, ...<tableName>Args } = <tableName>;and use<tableName>Argsas the mutation args. - Always enforce permissions: call
await assertPermission(ctx, "<tableName>:create")as the first line of the handler.
update — mutation
- Args:
{ id: v.id("<tableName>"), ...<tableName>Args }(shape fields minus slug). - Regenerate slug from name if applicable.
- Use
ctx.db.patch. - Always enforce permissions: call
await assertPermission(ctx, "<tableName>:update").
remove — mutation
- Args:
{ id: v.id("<tableName>") } - Use
ctx.db.delete. - Always enforce permissions: call
await assertPermission(ctx, "<tableName>:remove").
Import path depth
Adjust relative imports based on folder nesting:
- File in
convex/data/<tableName>.ts→../prefix (e.g.../_generated/server,../tables) - File in
convex/<tableName>.ts→./prefix
Validation Checklist
Before finishing, verify:
-
validators.ts— enum arrays added (if applicable), new table shape export added, included inexport default -
schema.ts— new import from./validatorsanddefineTableentry added - Functions file created at the correct path
- All five functions exported:
list,count,create,update,remove - Every function has argument validators (
v.*) -
slugis never accepted as a direct arg if it's auto-generated - No use of
.filter()in queries — usewithIndexonly -
.collect()is only used in thecountquery — use.paginate()or.take(n)everywhere else -
create,update, andremoveall callawait assertPermission(ctx, "<tableName>:<action>")as the first line of the handler -
assertPermissionis imported from../auth.helpers(adjust relative path based on folder depth) - Never accept a
userIdor user identifier as a function argument — derive auth viaassertPermission/getAuthUserserver-side