# Convex Backend — Implementation Rules

Applies to all files inside `convex/`. These rules govern how to write, organize,
and call Convex functions in this project.

---

## 1. Function Syntax

Always use the **new object syntax** with explicit `args` and `handler`:

```typescript
import { query } from "./_generated/server";
import { v } from "convex/values";

export const myFunction = query({
  args: { id: v.id("products") },
  handler: async (ctx, args) => {
    // ...
  },
});
```

---

## 2. Function Registration

| Visibility | Decorators to use |
|------------|------------------|
| Public API | `query`, `mutation`, `action` |
| Private / internal | `internalQuery`, `internalMutation`, `internalAction` |

- **Always include argument validators** on every function — public and internal alike.
- Do NOT register functions through the `api` or `internal` objects — those are for calling, not registering.
- If a function returns nothing, it implicitly returns `null`. Do not return `undefined`.

---

## 3. HTTP Endpoints

Define all HTTP endpoints in `convex/http.ts` using `httpAction`:

```typescript
import { httpRouter } from "convex/server";
import { httpAction } from "./_generated/server";

const http = httpRouter();

http.route({
  path: "/api/some-route",
  method: "POST",
  handler: httpAction(async (ctx, req) => {
    const body = await req.bytes();
    return new Response(body, { status: 200 });
  }),
});
```

Endpoints are registered at the exact path specified — no automatic prefix is added.

---

## 4. Function Calling

- `ctx.runQuery` — call a query from a query, mutation, or action
- `ctx.runMutation` — call a mutation from a mutation or action
- `ctx.runAction` — call an action from an action (only cross-runtime: V8 → Node)
- All calls take a **FunctionReference** — never pass the function directly
- Minimize query/mutation calls from actions; splitting logic creates race conditions
- When calling a function **in the same file**, add a type annotation on the return value to avoid TypeScript circularity errors:

```typescript
const result: string = await ctx.runQuery(api.example.f, { name: "Bob" });
```

---

## 5. Function References

- Public functions → `api` object from `convex/_generated/api.ts`
- Internal functions → `internal` object from `convex/_generated/api.ts`
- File-based routing: `convex/products.ts` → `api.products.myFn`; `convex/model/users.ts` → `api.model.users.myFn`

---

## 6. Validators

Use `v` from `convex/values`. Valid types:

| Convex Type | Validator | Notes |
|-------------|-----------|-------|
| Id | `v.id("tableName")` | |
| String | `v.string()` | UTF-8, max 1 MB |
| Number | `v.number()` | IEEE-754 float64 |
| Boolean | `v.boolean()` | |
| Null | `v.null()` | Use instead of `undefined` |
| Int64 | `v.int64()` | BigInt between -2^63 and 2^63-1 |
| Bytes | `v.bytes()` | ArrayBuffer, max 1 MB |
| Array | `v.array(values)` | Max 8192 elements |
| Object | `v.object({...})` | Max 1024 entries; keys cannot start with `$` or `_` |
| Record | `v.record(keys, values)` | Dynamic keys; ASCII only, no `$`/`_` prefix |
| Union | `v.union(...)` | Discriminated unions supported |
| Optional | `v.optional(...)` | |

- `v.bigint()` is **deprecated** — use `v.int64()` instead
- `v.map()` and `v.set()` are **not supported** — use `v.record()` instead

---

## 7. Schema

- Define all tables in `convex/schema.ts`
- Import from `convex/server`: `defineSchema`, `defineTable`
- System fields (`_id`, `_creationTime`) are added automatically — do not define them
- Index naming: always include all indexed fields — e.g. `["field1", "field2"]` → name `"by_field1_and_field2"`
- Index fields must be queried in the same order they are defined; create separate indexes for different orderings

---

## 8. TypeScript

- Use `Id<"tableName">` from `./_generated/dataModel` for document ID types — not plain `string`
- Use `Doc<"tableName">` for full document types
- Use `as const` for string literals in discriminated unions
- Declare arrays as `const arr: Array<T> = [...]`
- Declare records as `const rec: Record<K, V> = {...}`

---

## 9. Queries

- **Never use `.filter()`** — define an index in the schema and use `.withIndex()` instead
- Use `.unique()` to assert a single result (throws if multiple match)
- Use `.take(n)` or `.paginate()` to limit results
- Prefer `for await (const row of query)` for async iteration over `.collect()` + loop
- Convex does **not** support `.delete()` on queries — collect results then `ctx.db.delete(row._id)` each one
- Default sort order is ascending `_creationTime`; use `.order("asc" | "desc")` to override

### Full-text search

```typescript
const results = await ctx.db
  .query("products")
  .withSearchIndex("search_name", (q) =>
    q.search("name", "dog food").eq("category", "dogs"),
  )
  .take(10);
```

### Pagination

```typescript
import { paginationOptsValidator } from "convex/server";

export const list = query({
  args: { paginationOpts: paginationOptsValidator, category: v.string() },
  handler: async (ctx, args) => {
    return await ctx.db
      .query("products")
      .withIndex("by_category", (q) => q.eq("category", args.category))
      .order("desc")
      .paginate(args.paginationOpts);
  },
});
// Returns: { page, isDone, continueCursor }
```

---

## 10. Mutations

- `ctx.db.patch(id, fields)` — shallow merge; throws if document does not exist
- `ctx.db.replace(id, doc)` — full replace; throws if document does not exist
- `ctx.db.insert(table, doc)` — insert new document, returns `Id`
- `ctx.db.delete(id)` — delete document by id

---

## 11. Actions

- Add `"use node";` at the top of files whose actions use Node.js built-ins
- **Never** put `"use node";` in a file that also exports queries or mutations — separate the action into its own file
- `fetch()` is available in the default Convex (V8) runtime — no `"use node"` needed just for fetch
- **Never** use `ctx.db` inside an action — actions have no database access; use `ctx.runQuery` / `ctx.runMutation` instead
- Only call an action from another action when you need to cross runtimes; otherwise extract shared logic into a plain async helper

```typescript
"use node";
import { internalAction } from "./_generated/server";

export const myAction = internalAction({
  args: {},
  handler: async (ctx, args) => {
    // Node.js built-ins available here
    return null;
  },
});
```

---

## 12. Scheduling & Crons

- Use only `crons.interval` or `crons.cron` — do NOT use `crons.hourly`, `crons.daily`, or `crons.weekly`
- Both methods take a FunctionReference — never pass the function directly
- Export `crons` as `default` from `convex/crons.ts`
- Always import `internal` from `_generated/api` when a cron calls an internal function, even in the same file

```typescript
import { cronJobs } from "convex/server";
import { internal } from "./_generated/api";

const crons = cronJobs();

crons.interval("cleanup expired carts", { hours: 24 }, internal.carts.cleanupExpired, {});

export default crons;
```

---

## 13. File Storage

- `ctx.storage.getUrl(storageId)` — returns a signed URL or `null` if the file doesn't exist
- Do **not** use the deprecated `ctx.storage.getMetadata()` — query the `_storage` system table instead:

```typescript
const metadata = await ctx.db.system.get("_storage", args.fileId);
```

- Storage items are `Blob` objects — convert to/from `Blob` when reading or writing

---

## 14. API Design & File Organisation

- Use file-based routing to organise public functions logically: `convex/products.ts`, `convex/orders.ts`, etc.
- Extract shared business logic into `convex/model/*.ts` helpers and import them into public function files
- Keep `internalQuery`/`internalMutation`/`internalAction` in the same file as the public function that calls them, unless the file would exceed a manageable size
- Avoid chaining many `ctx.runQuery`/`ctx.runMutation` calls from a single action — race conditions become likely
