@mica/sdk (SDK contract v1)
    Preparing search index...

    Function useService

    • Talk to your own server service.

      Every other data hook — useNotes, useContacts, useMessages — is core code named after an app, backed by a store in web/src/services/ and a row per action in shared/routes.ts. That works for apps shipped in this repository and is unavailable to anybody else: an app installed from the Store cannot add a hook to the SDK, a store to core, or a route to the table. Which meant the add-on path supported UI-only apps, and sdk/coreBoundary.test.ts measures exactly how much of Notes and Blabber depends on being first-party.

      This is the general door. One NUI callback carries { service, action, data } and the client derives the event from the two segments, so an app reaches its own service without core knowing its name.

      A declaration (MICA-308), for an add-on whose server half is its own FiveM resource registered through exports.mica:RegisterService. The object you pass is the one your resource registers, so the UI's types and the server's validation read one source:

      // src/service.ts
      export const journal = defineAddonService({
      id: 'journal',
      actions: {
      create: { input: { title: { type: 'string', max: 80 } } },
      list: { input: {}, output: addonOutput<Entry[]>() }
      }
      });

      // anywhere in the app
      const entries = await useService(journal).call('list', {}, []); // Entry[]
      await useService(journal).call('create', { title }); // a typo in a name fails to compile

      It sends exactly what useService(journal.id) sends. The types are a convenience for the caller and a promise about nothing: the server parses every payload against the declaration it was handed, whatever this side believed.

      A string, for any service — a core app's own defineService table, or an add-on that has not written a declaration:

      const journal = useService('journal');
      const entries = await journal.call<Entry[]>('get', {}, []);
      await journal.call('create', { title, body });

      Authority. The server still authenticates the caller, rate-limits per (source, service, action) and parses the payload before a handler sees it. A NUI request was never proof of intent and is not now (§2.9) — this widens who can ask, not what the server agrees to.

      Whose service. The id — the string, or the declaration's id — is your own app id, or <appId>_<anything> for a second service of yours. A sandboxed add-on asking for another is refused by the shell before anything is sent; in this repo sdk/permissions.test.ts fails any other literal, any non-literal, and any declaration it cannot trace to a defineAddonService call inside the app's own directory.

      The named routes. They stay, and routes.test.ts keeps cross-referencing them against fetchNui calls, server registrations and the browser mock. That check catches the missing-layer bug that silently does nothing in game, and it is worth keeping for in-tree apps. This is the path for apps the table cannot cover.

      A call with no defaultValue rejects. A refusal the server keyed — a rate limit, an unauthenticated caller, an app the owner switched off, the generic failure — rejects with a ServiceRefusal whose message is ready to show and whose key is the catalog key it named, so ask isRefusal(error, 'server.rateLimited') rather than comparing the message, which is translated and may be reworded (MICA-310). Your own handler's addonError(...) carries no key and rejects with a plain Error. All of this holds in a sandboxed frame too: the key crosses postMessage beside the message, and nothing else of the error does.

      There is no store here, and that is deliberate rather than unfinished. createCrudStore and createPagedStore live in core because core's own services use them; an add-on holds its own state in its own module, which is the one part of being an add-on that is genuinely different rather than accidentally so.

      Type Parameters

      Parameters

      • declaration: D

      Returns DeclaredService<D>

    • Talk to your own server service.

      Every other data hook — useNotes, useContacts, useMessages — is core code named after an app, backed by a store in web/src/services/ and a row per action in shared/routes.ts. That works for apps shipped in this repository and is unavailable to anybody else: an app installed from the Store cannot add a hook to the SDK, a store to core, or a route to the table. Which meant the add-on path supported UI-only apps, and sdk/coreBoundary.test.ts measures exactly how much of Notes and Blabber depends on being first-party.

      This is the general door. One NUI callback carries { service, action, data } and the client derives the event from the two segments, so an app reaches its own service without core knowing its name.

      A declaration (MICA-308), for an add-on whose server half is its own FiveM resource registered through exports.mica:RegisterService. The object you pass is the one your resource registers, so the UI's types and the server's validation read one source:

      // src/service.ts
      export const journal = defineAddonService({
      id: 'journal',
      actions: {
      create: { input: { title: { type: 'string', max: 80 } } },
      list: { input: {}, output: addonOutput<Entry[]>() }
      }
      });

      // anywhere in the app
      const entries = await useService(journal).call('list', {}, []); // Entry[]
      await useService(journal).call('create', { title }); // a typo in a name fails to compile

      It sends exactly what useService(journal.id) sends. The types are a convenience for the caller and a promise about nothing: the server parses every payload against the declaration it was handed, whatever this side believed.

      A string, for any service — a core app's own defineService table, or an add-on that has not written a declaration:

      const journal = useService('journal');
      const entries = await journal.call<Entry[]>('get', {}, []);
      await journal.call('create', { title, body });

      Authority. The server still authenticates the caller, rate-limits per (source, service, action) and parses the payload before a handler sees it. A NUI request was never proof of intent and is not now (§2.9) — this widens who can ask, not what the server agrees to.

      Whose service. The id — the string, or the declaration's id — is your own app id, or <appId>_<anything> for a second service of yours. A sandboxed add-on asking for another is refused by the shell before anything is sent; in this repo sdk/permissions.test.ts fails any other literal, any non-literal, and any declaration it cannot trace to a defineAddonService call inside the app's own directory.

      The named routes. They stay, and routes.test.ts keeps cross-referencing them against fetchNui calls, server registrations and the browser mock. That check catches the missing-layer bug that silently does nothing in game, and it is worth keeping for in-tree apps. This is the path for apps the table cannot cover.

      A call with no defaultValue rejects. A refusal the server keyed — a rate limit, an unauthenticated caller, an app the owner switched off, the generic failure — rejects with a ServiceRefusal whose message is ready to show and whose key is the catalog key it named, so ask isRefusal(error, 'server.rateLimited') rather than comparing the message, which is translated and may be reworded (MICA-310). Your own handler's addonError(...) carries no key and rejects with a plain Error. All of this holds in a sandboxed frame too: the key crosses postMessage beside the message, and nothing else of the error does.

      There is no store here, and that is deliberate rather than unfinished. createCrudStore and createPagedStore live in core because core's own services use them; an add-on holds its own state in its own module, which is the one part of being an add-on that is genuinely different rather than accidentally so.

      Parameters

      • serviceId: string

      Returns {
          call: <T = unknown>(
              action: string,
              data?: unknown,
              defaultValue?: T,
          ) => Promise<T>;
          id: string;
      }

      • call: <T = unknown>(action: string, data?: unknown, defaultValue?: T) => Promise<T>

        Call one action and wait for the reply.

        defaultValue behaves as it does everywhere else in the SDK: a failed round trip resolves to it rather than throwing, so a missing server half degrades to an empty list instead of a crashed app. Omit it when a failure should surface — a write wrapped in useAppAction wants the error so it can toast it.

      • id: string