Internal Procedures
Server-only operations
Internal procedures (internalQuery and internalMutation) are server-only operations that are NOT exposed via HTTP. They're ideal for operations that should never be callable from clients.
Why Internal Procedures?
| Type | Exposed via HTTP | Use Case |
|---|---|---|
query | Yes | Client read operations |
mutation | Yes | Client write operations |
internalQuery | No | Server-only reads |
internalMutation | No | Server-only writes |
Internal Query
To create a server-only read operation, use t.internalQuery(). This demonstrates how you can define a procedure that is inaccessible from client-side code.
// src/api.ts
import { defineContext } from '@deessejs/server'
import { ok } from '@deessejs/fp'
const { t, createAPI } = defineContext({
context: () => ({ db: myDatabase }),
})
// Server-only: not accessible from client
const getSecretData = t.internalQuery({
handler: async (ctx) => {
const secrets = await ctx.db.secrets.findAll()
return ok(secrets)
},
})Internal Mutation
To create a server-only write operation, use t.internalMutation(). This shows how you can safely perform privileged operations that should never be exposed to clients.
// src/api.ts
import { ok } from '@deessejs/fp'
// Server-only: not accessible from client
const processPayment = t.internalMutation({
args: z.object({
userId: z.number(),
amount: z.number().positive(),
currency: z.string().default('USD'),
}),
handler: async (ctx, args) => {
const result = await ctx.payment.process({
userId: args.userId,
amount: args.amount,
currency: args.currency,
})
return ok(result)
},
})Use Cases
1. Secret Operations
When handling sensitive information like API keys, you should use internalQuery to keep secrets server-only. This prevents accidentally exposing privileged data to clients.
// Don't - secret exposed to client
const getApiKeys = t.query({
handler: async (ctx) => {
return ok(await ctx.db.apiKeys.findAll())
},
})
// Do - internal keeps secrets server-only
const getApiKeys = t.internalQuery({
handler: async (ctx) => {
return ok(await ctx.db.apiKeys.findAll())
},
})2. Admin Operations
When you need to perform administrative tasks like deleting users, use internalMutation to ensure these privileged operations are only callable from trusted server-side code.
const deleteUser = t.internalMutation({
args: z.object({ userId: z.number() }),
handler: async (ctx, args) => {
// Admin-only logic
await ctx.db.users.delete(args.userId)
return ok({ deleted: true })
},
})3. Cross-Service Communication
When other services need to trigger internal operations, use internalMutation to expose a secure entry point that clients cannot access.
// Called internally by other services
const syncExternalData = t.internalMutation({
args: z.object({ source: z.string() }),
handler: async (ctx, args) => {
const data = await fetchExternalData(args.source)
await ctx.db.externalData.upsert(data)
return ok({ synced: true })
},
})4. Database Transactions
When you need atomic operations across multiple tables, use internalMutation with transaction support. This lets you safely perform complex writes that must succeed or fail as a single unit.
const transferFunds = t.internalMutation({
args: z.object({
fromUserId: z.number(),
toUserId: z.number(),
amount: z.number().positive(),
}),
handler: async (ctx, args) => {
await ctx.db.transaction(async (trx) => {
await trx.users.decrementBalance(args.fromUserId, args.amount)
await trx.users.incrementBalance(args.toUserId, args.amount)
})
return ok({ success: true })
},
})Security Model
Internal procedures are excluded from the public API by design. This example shows how createPublicAPI only exports regular queries and mutations, keeping internal procedures server-only.
// Public API (createPublicAPI) only exports query and mutation
// internalQuery and internalMutation are excluded
export const api = createAPI({
router: appRouter,
})
// Only users.get, users.create, etc. are exposed
// internal procedures are NOT accessibleCalling Internal Procedures
When you need to call internal procedures from other procedures, you can invoke them directly through the context. This example shows how a public query can securely call internal operations.
// src/api.ts
const getUserStats = t.query({
args: z.object({ userId: z.number() }),
handler: async (ctx, args) => {
// Call internal procedure internally
const user = await ctx.db.internal.getUser(args.userId)
const orders = await ctx.db.internal.getOrders(args.userId)
return ok({ user, orders })
},
})Anti-Patterns
Don't Expose Sensitive Data via Query
When you accidentally expose sensitive fields like passwords or API keys in a public query, clients can access privileged information. Use internalQuery for sensitive data instead.
// Don't - sensitive data exposed
const getUser = t.query({
handler: async (ctx) => {
return ok({
password: ctx.user.password,
apiKeys: await ctx.db.apiKeys.findAll(),
})
},
})Don't Use internalQuery for Client Data
When a query result is needed by clients, use query instead of internalQuery. Internal queries are only for server-side operations, so using them for client-required data creates an accessibility problem.
// Don't - client needs this data
const getUserProfile = t.internalQuery({
handler: async (ctx) => {
return ok(await ctx.db.users.find(ctx.user.id))
},
})
// Do - use public query
const getUserProfile = t.query({
args: z.object({ id: z.number() }),
handler: async (ctx, args) => {
return ok(await ctx.db.users.find(args.id))
},
})Next Steps
- Routers - Organize procedures
- Security: Public vs Internal - Security model
- createPublicAPI - Create client-safe API