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.

@appbricx/define1 membersSDK referenceProject files: app.config.ts

On this page
  1. defineApp
  2. defineApp
  3. Types

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

NameTypeDescription
configrequiredAppConfig<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 profile must be a table in db/schema; with profile: { table, create: true } every other not-null column of that table needs a default.
Permissions
Evaluated in a sandbox with no clock, randomness or network.
Example
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