App config
app.config.ts: the app's brand, locale, sign-in, roles and their profile tables, config and secret names, outbound hosts, realtime and analytics. It runs in a sandbox with no clock, randomness or network and must export plain data.
Unknown keys are errors that name the closest key. A role's profile must be a table in db/schema. Config and secret values are never in this file: set them per environment with the secrets tool or the console's Variables page.
defineApp
defineAppfunction
Types app.config.ts: brand (name, tagline, theme, color, icon, ogImage), locale (language, currency, timezone), ui (tailwind: false turns Tailwind off), auth (providers, signup: "invite" or { as: [roles], default }, admin), roles (each { kind: "consumer" | "staff", label, description, profile }), config and secrets (names and rules, never values), http.allow, realtime (live tables and channels) and analytics (Google tag ids).
defineApp<R extends string>(config: AppConfig<R>): AppConfig<R>Parameters
| Name | Type | Description |
|---|---|---|
configrequired | AppConfig<R> | Plain data. The roles are the keys of roles; auth and realtime may only name those. |
- Returns
- The same object.
- Errors
- Unknown keys are check errors naming the closest key; a role's
profilemust be a table in db/schema; withprofile: { table, create: true }every other not-null column of that table needs a default.
- Unknown keys are check errors naming the closest key; a role's
- Permissions
- Evaluated in a sandbox with no clock, randomness or network.
import { defineApp } from "@appbricx/define";
export default defineApp({
brand: { name: "Teamday", tagline: "Team experiences, booked in a minute" },
locale: { language: "en-IN", currency: "INR", timezone: "Asia/Kolkata" },
auth: { providers: ["password", "google"], signup: { as: ["host", "hr"] } },
roles: {
host: { kind: "consumer", label: "Host", profile: "host_profile" },
hr: { kind: "consumer", label: "Company booker", profile: { table: "hr_profile", create: true } },
ops: { kind: "staff", label: "Teamday ops" },
},
config: { SUPPORT_PHONE: { type: "string", public: true } },
secrets: { PAYMENTS_KEY: { hosts: ["api.razorpay.com"], required: ["prod"] } },
http: { allow: ["api.razorpay.com"] },
realtime: { live: ["booking"] },
});Source: internal/kernel/appsdk/define/index.ts:153
Types
interface AppConfig<R extends string = string> {
brand: Brand;
locale?: Locale;
ui?: UI;
// The roles are R (their keys); auth and realtime name some of them (NoInfer:
// a subset there doesn't narrow R, a typo there is an error).
auth?: Auth<NoInfer<R>>;
roles: Record<R, Role>;
config?: Record<string, ConfigVar>;
secrets?: Record<string, SecretVar>;
http?: { allow?: string[] };
realtime?: Realtime<NoInfer<R>>;
analytics?: Analytics;
}Source: internal/kernel/appsdk/define/index.ts:129
interface Brand {
name: string;
tagline: string;
description?: string;
/** A theme preset (`appbricx theme list`; @appbricx/kit's palettes: "indigo", "harbor-blue", …). */
theme?: string;
/** `#RRGGBB`, over the theme's primary colour. */
color?: string;
/** `/public/…` or `https://…`: the browser-tab icon. */
icon?: string;
/** `/public/…` or `https://…`: the default Open Graph image. */
ogImage?: string;
}Source: internal/kernel/appsdk/define/index.ts:7
interface Locale {
/** BCP-47, e.g. "en-IN". */
language?: string;
/** ISO 4217, e.g. "INR". */
currency?: string;
/** IANA, e.g. "Asia/Kolkata". */
timezone?: string;
}Source: internal/kernel/appsdk/define/index.ts:21
interface UI {
/** Tailwind v4 over the app's code (default true; always on with @appbricx/kit). */
tailwind?: boolean;
/** @appbricx/kit's design direction (fonts, type scale, radius): "crisp-saas",
* "editorial", "playful", … (default: the theme's own). The theme is brand.theme. */
direction?: string;
}Source: internal/kernel/appsdk/define/index.ts:90
interface Auth<R extends string> {
/**
* The ways in. One left out is off: the platform refuses it and the
* sign-in screen doesn't offer it ("password" off: no password sign-in,
* sign-up or reset, except the demo sign-ins; "magicLink" off: no
* sign-in links). Without the list: password and magic links on, Google
* off. Open sign-up needs "password" or "google".
*/
providers?: Provider[];
/** Who may sign themselves up: "invite" (nobody), or the roles (the default first). */
signup?: "invite" | { as: R[]; default?: R };
/** The staff role the built-in admin sign-in acts as. */
admin?: R;
}Source: internal/kernel/appsdk/define/index.ts:30
type Provider = "password" | "google" | "magicLink";Source: internal/kernel/appsdk/define/index.ts:5
interface Role {
kind: "consumer" | "staff";
label?: string;
description?: string;
/**
* The table in db/ that holds this role's users' profiles. A name links
* an existing unlinked row (by email) and never inserts one;
* `{ table, create: true }` also creates the row (see Profile).
*/
profile?: string | Profile;
}Source: internal/kernel/appsdk/define/index.ts:61
interface Profile {
/** The table in db/ (its `user_id` column links the user). */
table: string;
/** The platform creates the row (default false: it only links one). */
create?: boolean;
}Source: internal/kernel/appsdk/define/index.ts:54
interface ConfigVar {
type: "string" | "number" | "boolean";
/** Readable in the browser. */
public?: boolean;
required?: Array<"dev" | "prod">;
}Source: internal/kernel/appsdk/define/index.ts:74
interface SecretVar {
required?: Array<"dev" | "prod">;
/** The only hosts the egress layer sends it to. */
hosts?: string[];
/** Allow ctx.secrets.get(name) (audited) besides ctx.secrets.ref(name). */
expose?: boolean;
}Source: internal/kernel/appsdk/define/index.ts:82
interface Realtime<R extends string> {
/**
* Tables (in db/) whose row changes stream to useLiveRows, useLive and the
* handle's live(). Each subscriber gets only the changes to rows it may read
* (re-read as them under row-level security); a table not listed is followed
* through the row-changed topics instead.
*/
live?: string[];
/** Channels by name (lower case letters, digits and _), at most 50. */
channels?: Record<string, Channel<R>>;
}Source: internal/kernel/appsdk/define/index.ts:117
interface Channel<R extends string> {
/** Who may join: "public" (signed-out visitors too), "signed_in" (the default) or these roles. */
access?: "public" | "signed_in" | R[];
/** Who may send: "members" (whoever joined, the default), "signed_in", "none" (server code only) or these roles. */
publish?: "members" | "signed_in" | "none" | R[];
/** Who is here, with joins and leaves (usePresence). Default false. */
presence?: boolean;
/**
* One channel per row of this table (in db/): joined as `"<name>:<row id>"`
* (`useChannel(app, "room", id)`), only by whoever can read that row.
*/
table?: string;
}Source: internal/kernel/appsdk/define/index.ts:102
interface Analytics {
/**
* Google tag ids (GA4 "G-…", Google Ads "AW-…", "GT-…", Floodlight "DC-…"; at most 5): every page
* loads gtag.js and configures each one, and in-app navigations send a page_view.
*/
google?: string[];
}Source: internal/kernel/appsdk/define/index.ts:145