# Query Users From a Func | Creght API for AI

> Read the project user directory in Creght Func with ctx.users.find and ctx.users.query: the user object fields, the 409 and OAuth traps in exact lookup, search/status/paging/ordering for lists, and the access gate you must implement yourself.

[![Creght](https://ugc.talizen.com/_assets/site/2061660904709165056/1780797461299__creght_logo.png)API for AI](/en.md)

[View llms.txt](/en/llms.txt)

Overview

- [Creght API for AI](/en/api.md)

Discoverability

- [How to optimize llms.txt](/en/api/optimize-llms-txt.md)

Site configuration

- [Implement domain-based locale routing](/en/api/domain-locale-routing.md)

Backend

- [Calling external APIs on the server and managing the cache](/en/api/ssr-external-api-cache.md)
- [Build site backend workflows with Func](/en/api/func-backend.md)
- [JSON tables: definition, reads, and queries](/en/api/func-json-tables.md)
- [Uploads: signed direct upload and Func-generated files](/en/api/func-assets-upload.md)
- [Timeouts and streaming responses](/en/api/func-timeout-streaming.md)
- [Integrate Alipay PC Web Payment with Func](/en/api/func-alipay-payment.md)

Integrations

- [Send Email and Verification Codes with Integrations](/en/api/func-email-integration.md)
- [Take Alipay payments with an integration](/en/api/func-alipay-integration.md)

Auth

- [Require a Verified Email to Sign Up](/en/api/auth-verified-registration.md)
- [Reset and Change Passwords](/en/api/auth-password-reset.md)
- [Sign In From a Func](/en/api/auth-func-login.md)
- [Query users from a Func](/en/api/func-user-directory.md)

On this page

- [First, separate ctx.auth from ctx.users](#scope)
- [The user object](#user-object)
- [Finding one person: find](#find)
- [Finding many: query](#query)
- [The gate: do not skip this section](#gate)
- [Custom profile fields](#profile)
- [When not to use it](#not-this)
- [Do not build your own user table](#do-not-build-your-own-user-table)
- [Do not use it for business queries](#do-not-use-it-for-business-queries)
- [Do not gate access in the page](#do-not-gate-access-in-the-page)
- [Do not use it as a session check](#do-not-use-it-as-a-session-check)
- [Acceptance checklist](#checklist)

Auth/Query users from a Func

# Query users from a Func

ctx.users is the project user directory: find resolves one person by identifier, query pages through the list. Covers the full user object, the three traps in find, the filter and ordering rules for query, and the section that matters most — the platform has no notion of roles, so the access gate is yours to write.

Copy Markdown link

User accounts are managed by the platform, and Func reaches them through `ctx.users` — the **project-wide user directory**: find one person by identifier, page through many, verify and reset passwords. It is a different thing from `ctx.auth`, which only answers "who made this call".

**Agent objective**

Read the directory through `ctx.users`, but every Func that touches it **must implement its own access gate**: the platform guarantees "users of this project", never "this caller is allowed to see them". Query results must not be returned to the browser as-is.

## First, separate ctx.auth from ctx.users

These two namespaces are the easiest to conflate, and the consequences are asymmetric: treating `ctx.users` as "the current user" means one wrong email changes somebody else's account.

| Namespace | Scope | Methods |
| --- | --- | --- |
| `ctx.auth` | **Whoever made this call**, resolved from the session cookie | `currentUser()` / `requireUser()` / `login()` / `register()` |
| `ctx.users` | **The whole project's user directory**, able to address anyone | `find()` / `query()` / `checkPassword()` / `setPassword()` |

> For any mutating call, **the identifier must come from a fact the server just confirmed**, not from a browser-supplied field. Changing the caller's own password is `ctx.users.setPassword({ userId: ctx.auth.requireUser().id, ... })`, never `{ email: input.email }` — the latter means "change whoever the browser claims to be".

## The user object

`ctx.auth.currentUser()`, `ctx.users.find()`, and `ctx.users.query()` all return the same structure. Field names are **snake\_case**:

| Field | Meaning |
| --- | --- |
| `id` | The user key, always present. **This is the only value that should be used as an ownership key** |
| `account` / `email` / `phone` | Three identifiers, any of which may be empty depending on how the user signed up |
| `name` / `avatar` | Display name and avatar URL. There is no `nickname` field |
| `status` | `enabled` or `disabled` |
| `profile` | Site-defined custom fields, see [Custom profile fields](#profile) |
| `last_login_at` / `created_at` / `updated_at` | Timestamps |

The object contains **no** password, session token, linked OAuth providers, or internal IDs. None of those enter the sandbox.

## Finding one person: find

`ctx.users.find(ref)` resolves exactly one user by identifier. `userId`, `email`, and `account` are **mutually exclusive** — passing two is a 400.

```typescript
import type { TalizenFuncContext } from 'talizen/func-runtime'

export function lookup(input, ctx: TalizenFuncContext) {
  const user = ctx.users.find({ email: input.email })
  if (!user) return { ok: false }   // missing returns null, it does not throw
  return { ok: true, name: user.name }
}
```

Three traps:

- **A missing user is `null`, not an error.** "Does this person exist" is a normal branch for site code, not something to wrap in try/catch.
- **One email may belong to several users**, and `find` throws 409 rather than picking a row. Projects that allow duplicate emails must look up by `account` or `userId`.
- **`account` only resolves users with a password identity.** An OAuth-only account has no password identity row and cannot be found by `account` — use `email` or `userId`.

> Exposing the result of `find` as "is this email registered" is an account-enumeration hole. In anonymous-facing flows such as password reset and signup, **both branches must return exactly the same thing** — see [Reset and Change Passwords](/api/auth-password-reset.md).

## Finding many: query

`ctx.users.query(query)` returns users by page, in the same shape as `ctx.db.query`: `{ total, list, limit }`.

```typescript
export function directory(input, ctx: TalizenFuncContext) {
  requireAdmin(ctx)   // see the next section; this line is not optional

  const result = ctx.users.query({
    search: input.keyword,        // matches account / email / phone / name
    status: 'enabled',            // 'enabled' | 'disabled'; omit for no filter
    order_by: 'created_at desc',
    limit: 20,
    offset: (input.page - 1) * 20,
  })

  // Hand out only the fields the page actually needs
  return {
    total: result.total,
    list: result.list.map((user) => ({
      id: user.id,
      name: user.name,
      createdAt: user.created_at,
    })),
  }
}
```

| Parameter | Meaning |
| --- | --- |
| `search` | Substring match across `account`, `email`, `phone`, and `name`. Omit for no filter |
| `status` | `enabled` or `disabled`; anything else is a 400 |
| `limit` | **Default 20, maximum 100**, clamped silently; the returned `limit` is what actually applied |
| `offset` | Paging offset |
| `order_by` | `created_at`, `last_login_at`, or `id`, each optionally `asc`/ `desc`. Default `created_at desc`; any other column is a 400 |

**Users returned by `query` carry no `profile`.** Custom fields may hold back-office-only content, and a list result is the thing most likely to be forwarded wholesale to the browser. When you need one person's full record, take the `id` and call `find`.

> There are no aggregates, no grouping, and no filtering by `profile` fields. To select people by a business fact ("everyone who bought a course"), write that fact into your own [JSON table](/api/func-json-tables.md) keyed by `user.id` and query the table. The user directory is not a business database.

## The gate: do not skip this section

`find` has a natural barrier — you must already know an identifier. **`query` has none**: it makes the directory something you can page through. A Func that forgets the access check is a public customer-list export endpoint.

```typescript
// The platform does not know who your admins are; this check is yours to write
function requireAdmin(ctx: TalizenFuncContext) {
  const user = ctx.auth.requireUser()          // 1. must be signed in
  const admin = ctx.db.get('admins', user.id)  // 2. must be in your own grant table
  if (!admin) throw new Error('forbidden')
  return user
}
```

- **Every Func calling `query` must call `requireUser()` first and then decide whether this person qualifies.** The platform does project isolation only; it has no notion of roles.
- **Do not return `result.list` to the browser directly.** User objects carry emails and phone numbers — pick the fields the page needs.
- Model "admin" with your own JSON table keyed by `user.id`. Do not hard-code a rule such as an email-domain suffix.
- Directory reads are never cached: results reflect live account rows.

## Custom profile fields

Besides the built-in fields, each project can define custom user fields. The schema is configured in the editor under **Backend → Users**. Every field carries two switches:

| Switch | Meaning |
| --- | --- |
| `x-customer-readable` | Whether the browser can read the field (default: yes) |
| `x-customer-writable` | Whether the browser can write the field ( **default: no**) |

Both switches constrain the **browser**. Func is server code, so `find()` returns the **unfiltered profile** — including fields marked `x-customer-readable: false`. Returning `user.profile` wholesale from a Func therefore bypasses the switch.

```typescript
export function me(input, ctx: TalizenFuncContext) {
  const user = ctx.users.find({ userId: ctx.auth.requireUser().id })

  // Do not return { profile: user.profile } — it may contain internal-only fields
  return { plan: user.profile?.plan ?? 'free' }
}
```

**`query()` omits `profile` entirely**, precisely so that casually forwarding a list cannot become a bulk leak. For one person's full record, call `find` with their `id`.

> **Func cannot modify a profile today.** It is writable only at account creation through `ctx.auth.register({ profile })`; there is no update path afterwards. Data that changes — plan state, credits, preferences — belongs in your own [JSON table](/api/func-json-tables.md) keyed by `user.id`, leaving profile for the few attributes fixed at signup.

## When not to use it

### Do not build your own user table

Never create identity tables such as `users` or `auth_users`, and never use an email as a business key. Accounts, passwords, sessions, and OAuth are platform capabilities — see [Sign In From a Func](/api/auth-func-login.md).

### Do not use it for business queries

The directory filters only by identifier and the few built-in fields. Business-dimension filtering belongs in your own JSON table keyed by `user.id`.

### Do not gate access in the page

"Only admins see this list" must be decided inside the Func. Hiding UI in the browser is presentation only; the endpoint remains directly callable.

### Do not use it as a session check

Always use `ctx.auth` to answer "who is calling". `ctx.users.find` can return anyone's object and proves nothing about the caller.

## Acceptance checklist

- Every Func using `ctx.users` calls `requireUser()` and checks authorization beyond that.
- What reaches the browser is a selected set of fields — never a whole user object, never a whole `profile`.
- Mutating calls take an identifier confirmed by the server, not an email supplied by the browser.
- Anonymous-facing flows return identical responses whether or not the user exists.
- List endpoints pass `limit` and `offset` and page through `total`.
- Changing user data lives in a JSON table keyed by `user.id`, not in `profile` (read-only from Func) and not in a second identity table.

**Completion criteria**

Every read of the user directory can answer who is reading, on what authority, and whether the data returned exceeds what the page needs — and calling the same Func as an unauthorized account is clearly refused.

![Creght](https://ugc.talizen.com/_assets/site/2061660904709165056/1780797461299__creght_logo.png)

This website is built with [Creght](/en.md)

![WeChat Support](https://fsu.creght.com/site/2066727200882692096/1785119134612__image.png)

WeChat Support

## Links

- [Pricing](/en/price.md)
- [Solutions](/en/solution.md)
- [Customers](/en/customers.md)
- [Help Center](/en/help.md)
- [Contact Us](/en/contact.md)
- [Update Logs & Blogs](/en/blogs.md)
- [Refund Policy](/en/tuikuan.md)

## Resources

- [All Resources](/en/resources.md)
- [Templates](/en/templates.md)
- [Components](https://creghtlib.site.creght.com)
- [Animations](/en/design/effects.md)
- [Figma to Creght](/en/figma2creght.md)
- [API for AI](/en/api.md)

## Comparisons

- [vs Shianxianle](/en/creght-vs-sxl.md)
- [vs Fanke](/en/creght-vs-fkw.md)
- [自己写代码 vs Creght](/en/compare/self-coding.md)
- [外包 vs 自己做](/en/compare/outsourcing.md)

## Terms

- [Terms of Service](/en/legal/terms.md)
- [Privacy Policy](/en/legal/privacy.md)
- [Acceptable Use Policy](/en/legal/acceptable-use.md)

## Social Media

- [Xiaohongshu](https://www.xiaohongshu.com/user/profile/5a38606811be10715f4895b6)
- [Bilibili](https://space.bilibili.com/513308095)

[蜀ICP备2023038192号-2](https://beian.miit.gov.cn)
