This is the full developer documentation for Crowdin Serverless Apps
# Crowdin
Serverless Apps
> Build Crowdin apps without a backend - develop, preview, and publish right from your terminal
## Next steps
[Section titled “Next steps”](#next-steps)
Build with your AI agent
The [create-app skill](/serverless-apps/getting-started/quick-start/#install-the-crowdin-plugin) teaches your agent to scaffold, develop, and publish an app end to end - you only describe what it should do.
Quick start
Install the [CLI](/serverless-apps/reference/cli-commands/), scaffold an app, and see it running inside Crowdin in minutes. Follow the [quick start guide](/serverless-apps/getting-started/quick-start/).
Nothing to host
A serverless app is a static JavaScript bundle served by Crowdin. No servers, no tokens, no infrastructure - Crowdin injects the context and executes API calls on the user’s behalf.
Typed SDK
The [SDK](/serverless-apps/building-app/overview/) registers your modules and gives you typed context, host events and actions, a tokenless Crowdin API client, a UI kit styled to match Crowdin, and an i18n runtime.
Zero-config toolchain
React, Tailwind CSS, and Lingui i18n work out of the box. The [CLI](/serverless-apps/reference/cli-commands/) runs your app live inside Crowdin with hot reload and publishes it when you are ready.
Docs for LLMs
We support the [llms.txt](https://llmstxt.org/) convention for making documentation available to large language models. Check out [this site’s llms.txt file](/serverless-apps/llms.txt) for a full list of available files.
# 404
> Page not found. Check the URL or try using the search bar.
# Static Assets
> Ship images and other static files with a serverless app bundle and reference them at runtime
Files in the app’s `public/` directory are copied into `dist/` by the build and shipped inside `bundle.zip`, so they are served from the same base URL as `app.js` - the bundle root. The dev server serves them from the same paths, so assets behave identically in development and after publishing.
## Referencing assets at runtime
[Section titled “Referencing assets at runtime”](#referencing-assets-at-runtime)
`getAssetUrl` resolves a root-relative path against the bundle’s base URL, whatever the [bundle mode](/serverless-apps/development/dev-and-publish/) currently is:
```ts
import { getAssetUrl } from "@crowdin/serverless-apps-sdk";
const logo = getAssetUrl("/logo.svg");
```
Always build asset URLs this way - the app’s page URL is `…/embed/` while bundle assets live under the bundle root, a different path (and, in external and dev modes, a different origin), so document-relative paths like `` never hit the bundle. The leading slash is optional: `getAssetUrl("/logo.svg")` and `getAssetUrl("logo.svg")` resolve to the same file, matching the manifest’s `logo` convention.
## Assets the manifest references
[Section titled “Assets the manifest references”](#assets-the-manifest-references)
The `logo` fields in the [manifest](/serverless-apps/reference/manifest/) (top-level and per module) use the same space: `"/logo.svg"` means `public/logo.svg` served from the bundle root.
## What belongs in public/
[Section titled “What belongs in public/”](#what-belongs-in-public)
Use `public/` for files that must keep their own URL: logos referenced by the manifest and files your app fetches at runtime. Assets imported from source code are handled by the build itself. Translation catalogs (`dist/locales/*.json`) also live in the bundle, but they are generated by the [i18n pipeline](/serverless-apps/building-app/i18n/), not by you.
# Context, Theme, and Events
> Read the host context, sync the Crowdin theme, and react to host events
The SDK tells your app where it is rendered (context), keeps it in the host’s theme, and notifies it about what happens around it (events):
```tsx
import {
AppUiProvider,
useCrowdinContext,
useCrowdinEvent,
} from "@crowdin/serverless-apps-sdk/react";
function App() {
return (
);
}
function Panel() {
const context = useCrowdinContext();
useCrowdinEvent("language.change", () => {
// refresh whatever depends on the selected language
});
return
Project: {context.project?.id}
;
}
```
## Context fields
[Section titled “Context fields”](#context-fields)
`useCrowdinContext()` (and `getContext()` in the core) return a `CrowdinContext`:
| Field | Type | Description |
| --------------- | ----------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `app` | `{ id, type, key }` | The app id, the module type being rendered, and its manifest `key` |
| `user` | `{ locale, timezone, id?, login? }` | The current Crowdin user. `locale`/`timezone` can be `null`; `id` and `login` are present only for an authenticated viewer |
| `project` | `{ id, identifier? }` or `null` | The current project, when the module renders in a project context. `identifier` is present when the host page knows it |
| `organization` | `{ id, domain? }` or `null` | The current organization, when there is one. `domain` is present on Crowdin Enterprise only |
| `isEnterprise` | `boolean` | Whether the host is Crowdin Enterprise |
| `parentOrigin` | `string` or `null` | Origin of the hosting Crowdin page |
| `assetsBaseUrl` | `string` or `null` | Base URL the app’s bundle assets are served from |
`user.locale` is a normalized [BCP-47](https://developer.mozilla.org/en-US/docs/Glossary/BCP_47_language_tag) tag (`en-US`, `uk-UA`), so it is safe to pass to `Intl.DateTimeFormat`, `Intl.NumberFormat`, `toLocaleDateString`, and friends.
## Events
[Section titled “Events”](#events)
`useCrowdinEvent(name, handler)` is fully typed - the payload type follows the event name. The core `events.on(name, handler)` accepts the same events with an untyped payload; cast it with the exported payload types (e.g. `TranslationObject`). These are the host events of the Crowdin Apps platform; the payloads are described in the [supported events reference](https://support.crowdin.com/developer/crowdin-apps-js/#supported-events).
The table lists the events typed in this SDK version. The platform can start emitting new events at any time, and they work on any SDK version right away - both `events.on` and `useCrowdinEvent` accept any event name string.
| Event | Fires when |
| -------------------------- | -------------------------------------------------------- |
| `string.change` | The active source string changes |
| `string.selected` | The string selection changes (multiple selection) |
| `textarea.edited` | The translation textarea content is edited |
| `translation.added` | A translation is suggested |
| `translation.deleted` | A translation is deleted |
| `translation.restored` | A translation is restored |
| `translation.vote` | A translation is voted on |
| `translation.approve` | A translation is approved |
| `translation.disapprove` | A translation approval is revoked |
| `language.change` | The target language changes |
| `file.change` | The active file changes |
| `asset.source.preview` | The asset source preview is shown |
| `asset.suggestion.preview` | An asset suggestion preview is shown |
| `theme.changed` | The host switches between light and dark theme |
| `intersection.changed` | A registered intersection observer reports a change |
| `context.menu.click` | The user activates one of the app’s context-menu entries |
## Theme
[Section titled “Theme”](#theme)
`AppUiProvider` syncs the host theme: it toggles dark mode and applies Crowdin’s CSS variables, so the [`/ui` components](/serverless-apps/building-app/user-interface/) automatically match the Crowdin look.
Inside `AppUiProvider`, the `useTheme()` hook returns the current theme (`{ mode: "light" | "dark" }`) and re-renders when the host switches theme - use it for custom rendering that the CSS variables don’t cover.
## Without React
[Section titled “Without React”](#without-react)
The framework-free core offers the same capabilities without React: `getContext()`, `getTheme()`, `onThemeChange()`, `events.on()`, plus the typed [host actions](/serverless-apps/building-app/host-actions/) such as `editor`, `project`, `profile`, and `modal`.
```ts
import { getContext, events } from "@crowdin/serverless-apps-sdk";
const context = getContext();
events.on("language.change", () => {
// refresh whatever depends on the selected language
});
```
# Crowdin API
> Call the Crowdin REST API from a serverless app without any tokens
```ts
import { createCrowdinClient } from "@crowdin/serverless-apps-sdk/api";
const client = createCrowdinClient();
const strings = await client.sourceStringsApi.listProjectStrings(projectId);
```
`createCrowdinClient()` returns a [`@crowdin/crowdin-api-client`](https://www.npmjs.com/package/@crowdin/crowdin-api-client) instance whose requests are executed by the Crowdin host under the current user’s session - no tokens ever reach the app. See the [Crowdin REST API reference](https://support.crowdin.com/developer/api/) for the available endpoints.
## Scopes
[Section titled “Scopes”](#scopes)
Access is limited to the [scopes](https://support.crowdin.com/developer/understanding-scopes/) declared in the [manifest](/serverless-apps/reference/manifest/). An empty `scopes` array means no API access.
## Uploading files
[Section titled “Uploading files”](#uploading-files)
Raw request bodies work like with any other transport, so the Storage API accepts files directly:
```ts
const file = new Blob([docxBytes]);
const storage = await client.uploadStorageApi.addStorage("guide.docx", file);
await client.sourceFilesApi.createFile(projectId, {
name: "guide.docx",
storageId: storage.data.id,
});
```
`addStorage` accepts a `Blob`/`File`, an `ArrayBuffer`, a typed array, or a string. `FormData` is the one body type that is not supported - no Crowdin API v2 endpoint requires multipart; pass the raw file instead.
## Streaming endpoints
[Section titled “Streaming endpoints”](#streaming-endpoints)
Endpoints that respond with a stream (such as AI chat completions with `stream: true`) resolve once the stream completes, with the full response as a string. This matches how the client behaves with its native transports - the `HttpClient` contract has no channel for incremental delivery, so there is no progressive streaming with tokens arriving one by one.
## Limitations
[Section titled “Limitations”](#limitations)
* GraphQL is not available - the host bridge proxies the REST API (`/api/v2`) only.
* `FormData` bodies are rejected; pass raw files instead.
* Response headers are not exposed - the client resolves with the parsed body only, like its native transports.
* Errors surface as the client’s own `CrowdinError`/`CrowdinValidationError`, same as with a token.
# Host Actions
> Drive the Crowdin UI from a serverless app - editor actions, navigation, modals, notifications, and sizing
Besides [context and events](/serverless-apps/building-app/context/), the SDK exposes the host’s action API: typed functions that make the surrounding Crowdin UI do things. Everything on this page is exported from the framework-free core entry point:
```ts
import { editor, project, modal, redirect, resize } from "@crowdin/serverless-apps-sdk";
```
| Export | Available in | What it covers |
| ---------------- | ---------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `editor` | editor modules | \~50 methods: strings and translations, filters and search, navigation, notifications, workflow steps, the app’s modal |
| `project` | project-scoped modules | navigation inside the project area, opening the editor |
| `profile` | profile-scoped modules | navigation inside the profile area |
| `modal` | `modal` modules | resizing the open modal |
| global functions | everywhere | theme, sizing, redirects, [assets](/serverless-apps/building-app/assets/), intersection observer |
Every method is fully typed, so the definitive reference is your editor’s autocomplete on these exports. The sections below cover the calls most apps need. A method only works where its UI exists: outside it, fire-and-forget calls are silently ignored, while promise-returning calls reject with an `AP method … is unavailable` error - guard with `try`/`catch` or a `context.app.type` check when one bundle serves several placements.
## Editor content
[Section titled “Editor content”](#editor-content)
Read what the translator is working on and change the translation draft:
```ts
import { editor } from "@crowdin/serverless-apps-sdk";
const active = await editor.getString();
const translations = await editor.getTranslations();
editor.setTranslation("Bonjour");
editor.appendTranslation(" le monde");
editor.clearTranslation();
```
`getStringsList()` returns the currently loaded page of strings, `getSelectedStrings()` the multi-selection (or `"all"`). Pair these with the [`string.change` and `string.selected` events](/serverless-apps/building-app/context/#events) to stay in sync as the translator moves around.
For suggesting translations in bulk there is the unsaved-suggestions family: `setUnsavedSuggestion` / `setUnsavedSuggestions`, `removeUnsavedSuggestions`, and `applyUnsavedTranslations` fill translation drafts without saving them, leaving the translator in control.
## Filters, search, and navigation
[Section titled “Filters, search, and navigation”](#filters-search-and-navigation)
```ts
editor.search("checkout");
editor.setCroqlFilter('updated_at > "2026-01-01"');
editor.setFilter(2);
editor.changeFile(fileId);
editor.setTargetLanguage("uk");
```
`getFiltersList()` enumerates the editor’s built-in filters for `setFilter`; `setCustomFilter` and `setCroqlFilter` (with their `reset*` counterparts) apply advanced filters; `setPage`, `setMode`, and `setWorkflowStep` move the editor view.
## Notifications
[Section titled “Notifications”](#notifications)
```ts
editor.successMessage("Translations imported");
editor.errorMessage("Import failed");
editor.setApplicationNotification(3);
```
The `*Message` methods show a toast in the editor; `setApplicationNotification` / `clearApplicationNotification` manage the counter badge on your app’s panel icon.
## Modals
[Section titled “Modals”](#modals)
A [`modal` module](/serverless-apps/building-app/modules/) is rendered when the host opens it. From an editor module, open your app’s modal by its manifest `key`:
```ts
const opened = await editor.openModal({ resource: "details", size: "large" });
```
`resource` is the key of one of your app’s `modal` modules; the call resolves `false` when no such module exists. Sizes are `small`, `medium` (default), `large`, and `xlarge`.
Inside the modal, the same bundle renders with `context.app.type === "modal"`. From there:
```ts
import { modal, closeAppModal } from "@crowdin/serverless-apps-sdk";
modal.setSize({ width: 800, height: 600 });
closeAppModal();
```
Context-menu entries with `"type": "modal"` open modals the same way - declaratively, via the [manifest](/serverless-apps/reference/manifest/#options-context-menu).
## Sizing
[Section titled “Sizing”](#sizing)
```ts
resize();
resize(400, 600);
const viewport = await getViewportSize();
```
`resize()` without arguments fits the iframe to its content. `getViewportSize()`, `getWindowSize()`, and `getScrollPosition()` report the host page’s dimensions; `getSize()` (and its synchronous twin `getSizeSync()`) returns the app iframe’s own content size instead.
## Navigation
[Section titled “Navigation”](#navigation)
```ts
import { redirect, project } from "@crowdin/serverless-apps-sdk";
redirect("/project/my-project/settings");
project.openEditor(fileId);
project.redirect("/tools");
```
The global `redirect(path, queryParams?)` navigates the top Crowdin page; `project.redirect` and `profile.redirect` navigate within their areas; `project.openEditor(fileId, view?, languageCode?)` jumps straight into the editor.
## Advanced
[Section titled “Advanced”](#advanced)
* `editor.registerContextMenuAction(action, handler)` adds an entry to the editor’s context menus at runtime and returns an unsubscribe function - unlike manifest-declared [`context-menu` modules](/serverless-apps/reference/manifest/#options-context-menu), which are static.
* `registerIntersectionObserver(handler)` reports when the app’s iframe scrolls in and out of view (the `intersection.changed` event).
* `closeNavbarExtension()` closes the app’s `navbar-extension` flyout.
* `editor.getHotKeys()` / `editor.propagateHotKeyPress()` integrate the app’s textareas with the editor’s keyboard shortcuts.
* `editor.applyCustomThemeStyle()` / `editor.resetCustomThemeStyle()` apply and clear editor color-theme `--crowdin-*` style overrides programmatically.
* `getFormData()` / `formDataUpdated(detail)` / `getSchema()` / `getRenderData()` exchange data with a form the Crowdin host renders around the app in some placements; where the host renders no form, they resolve empty data.
* `getCssVariables()` returns the host’s `--crowdin-*` CSS variables - [`AppUiProvider`](/serverless-apps/building-app/context/#theme) applies them for you.
# Translating Your App (i18n)
> Translate the UI of a serverless app with the zero-config Lingui pipeline and runtime
App localization is zero-config end to end: write UI strings with [Lingui](https://lingui.dev/) macros, extract them into `.po` catalogs, and the compiled translations ship with the bundle and load at runtime for the Crowdin user’s locale.
## Writing and extracting strings
[Section titled “Writing and extracting strings”](#writing-and-extracting-strings)
Write strings with Lingui macros and wrap the app in `AppI18nProvider`:
```tsx
import { AppI18nProvider } from "@crowdin/serverless-apps-sdk/i18n";
import { Trans } from "@lingui/react/macro";
function Root() {
return (
Loading…}>
Hello from my app!
);
}
```
Then run the [CLI](/serverless-apps/reference/cli-commands/#extract):
```bash
crowdin-serverless-apps extract
crowdin-serverless-apps build
```
* `extract` collects the strings into `locales/.po` catalogs - translate those (in Crowdin, naturally);
* `build` and `dev` compile the catalogs to `dist/locales/.json`, which ship inside the bundle as [runtime assets](/serverless-apps/building-app/assets/).
## Runtime behavior
[Section titled “Runtime behavior”](#runtime-behavior)
At runtime, `AppI18nProvider` detects the Crowdin user’s locale from the [host context](/serverless-apps/building-app/context/) and loads the matching catalog from the bundle. If no catalog exists for the user’s locale, the source locale is used (default `en-US`, configurable via the `sourceLocale` prop).
## Catalog naming
[Section titled “Catalog naming”](#catalog-naming)
Name catalogs after full Crowdin locale codes (`uk-UA.po`, `pt-BR.po`) - the CLI warns about names Crowdin will never request (such as `uk.po`), and the runtime never falls back to base languages.
The Crowdin UI ships in a fixed set of locales, so these are the only catalogs the host will ever request:
`en-US`, `ar-SA`, `be-BY`, `cs-CZ`, `da-DK`, `de-DE`, `es-ES`, `fr-FR`, `hu-HU`, `it-IT`, `ja-JP`, `pl-PL`, `pt-BR`, `pt-PT`, `ru-RU`, `sk-SK`, `tr-TR`, `uk-UA`, `xh-ZA`, `zh-CN`, `zu-ZA`
## Translating the catalogs in Crowdin
[Section titled “Translating the catalogs in Crowdin”](#translating-the-catalogs-in-crowdin)
The natural loop is to translate the app in a Crowdin project of its own:
1. Create a project and upload `locales/en-US.po` as the source file.
2. Set the file’s resulting-file-name pattern to produce full locale codes, e.g. `/locales/%locale%.po` - the default two-letter `%two_letters_code%` would export `uk.po`, which the runtime never requests.
3. Translate, then download the built translations into the app’s `locales/` directory.
4. Re-run `build` and `publish` - the new catalogs ship with the bundle.
## Notes
[Section titled “Notes”](#notes)
* The pipeline needs no `lingui.config.ts`. If you add one, keep the PO format and the `locales/{locale}` catalog layout - see [customization](/serverless-apps/development/customization/).
* Catalogs are compiled once when `dev` starts; restart the dev server after editing `.po` files.
# Registering Modules
> How a serverless app bundle registers the UI modules its manifest declares
One bundle serves every module your manifest declares. Register each module at the top level of the entry file with the matching `prepare*` function; when Crowdin renders the app, the SDK invokes the registered module that matches the host context:
```tsx
import { prepareProjectMenu } from "@crowdin/serverless-apps-sdk";
import { createRoot } from "react-dom/client";
import { App } from "./App";
prepareProjectMenu({
render() {
createRoot(document.getElementById("root")!).render();
},
});
```
Caution
Registration must happen synchronously at the top level of the bundle - not inside an async callback.
If the manifest declares several modules of the same type, pass the module key as the second argument: `prepareProjectMenu({ … }, "my-key")`.
## All module types
[Section titled “All module types”](#all-module-types)
The table lists every module type this SDK version supports. The set grows with the Crowdin platform, and a new type always arrives together with an SDK release (the matching `prepare*` function and manifest schema entry), so [keep `@crowdin/serverless-apps-sdk` up to date](/serverless-apps/building-app/overview/#upgrading) to pick up new placements.
| Manifest module type | Function | Crowdin reference |
| ------------------------------- | ------------------------------------ | ------------------------------------------------------------------------------------------------------- |
| `editor-right-panel` | `prepareEditorRightPanel` | [module docs](https://support.crowdin.com/developer/crowdin-apps-module-editor-right-panel/) |
| `editor-translations-panel` | `prepareEditorTranslationsPanel` | [module docs](https://support.crowdin.com/developer/crowdin-apps-module-editor-translations-panel/) |
| `editor-asset-panel` | `prepareEditorAssetPanel` | [module docs](https://support.crowdin.com/developer/crowdin-apps-module-editor-asset-panel/) |
| `editor-background-worker` | `prepareEditorBackgroundWorker` | - |
| `project-tools` | `prepareProjectTools` | [module docs](https://support.crowdin.com/developer/crowdin-apps-module-project-tools/) |
| `project-menu` | `prepareProjectMenu` | [module docs](https://support.crowdin.com/developer/crowdin-apps-module-project-menu/) |
| `project-menu-crowdsource` | `prepareProjectMenuCrowdsource` | [module docs](https://support.crowdin.com/developer/crowdin-apps-module-project-menu-crowdsource/) |
| `project-reports` | `prepareProjectReports` | [module docs](https://support.crowdin.com/developer/crowdin-apps-module-project-reports/) |
| `project-integrations` | `prepareProjectIntegrations` | [module docs](https://support.crowdin.com/developer/crowdin-apps-module-project-integrations/) |
| `profile-resources-menu` | `prepareProfileResourcesMenu` | [module docs](https://support.crowdin.com/developer/crowdin-apps-module-profile-menu/) |
| `profile-settings-menu` | `prepareProfileSettingsMenu` | [module docs](https://support.crowdin.com/developer/crowdin-apps-module-profile-settings-menu/) |
| `organization-menu` | `prepareOrganizationMenu` | [module docs](https://support.crowdin.com/developer/crowdin-apps-module-organization-menu/) |
| `organization-settings-menu` | `prepareOrganizationSettingsMenu` | [module docs](https://support.crowdin.com/developer/crowdin-apps-module-organization-settings-menu/) |
| `organization-menu-crowdsource` | `prepareOrganizationMenuCrowdsource` | [module docs](https://support.crowdin.com/developer/crowdin-apps-module-organization-menu-crowdsource/) |
| `modal` | `prepareModal` | [module docs](https://support.crowdin.com/developer/crowdin-apps-module-modal/) |
| `chat` | `prepareChat` | [module docs](https://support.crowdin.com/developer/crowdin-apps-module-chat/) |
| `context-menu` | `prepareContextMenu` | [module docs](https://support.crowdin.com/developer/crowdin-apps-module-context-menu/) |
| `navbar-extension` | `prepareNavbarExtension` | - |
The linked references describe where each module appears in the Crowdin UI and how it behaves. Their manifest examples are written for classic (backend) Crowdin apps - for serverless apps, declare modules as described in the [manifest reference](/serverless-apps/reference/manifest/).
Two types have no reference page yet:
* `editor-background-worker` runs in the editor without any visible panel - useful for reacting to [editor events](/serverless-apps/building-app/context/#events) and preparing suggestions in the background.
* `navbar-extension` is a flyout panel opened from the Crowdin navigation bar; close it programmatically with [`closeNavbarExtension()`](/serverless-apps/building-app/host-actions/#advanced).
# SDK Overview
> Typed SDK for building serverless Crowdin apps - frontend-only apps that run inside the Crowdin UI
[`@crowdin/serverless-apps-sdk`](https://www.npmjs.com/package/@crowdin/serverless-apps-sdk) is the typed SDK for building serverless Crowdin apps - frontend-only apps that run inside the Crowdin UI. The SDK is your bundle’s connection to Crowdin: it registers the app’s modules, exposes typed context, events, and host actions, and ships a Crowdin REST API client that works without tokens, Crowdin-hosted app storage, a UI kit styled to match Crowdin, and an i18n runtime.
## Installation
[Section titled “Installation”](#installation)
* npm
```sh
npm i @crowdin/serverless-apps-sdk
```
* pnpm
```sh
pnpm add @crowdin/serverless-apps-sdk
```
* yarn
```sh
yarn add @crowdin/serverless-apps-sdk
```
`react`, `react-dom`, and the Lingui packages are optional peer dependencies: the core entry point works without them; install them to use the `/react`, `/ui`, or `/i18n` entry points. Apps scaffolded with the [CLI](/serverless-apps/reference/cli-commands/) come with all of this preconfigured.
## Entry points
[Section titled “Entry points”](#entry-points)
| Import | Contents |
| --------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `@crowdin/serverless-apps-sdk` | Framework-free core: [module registration](/serverless-apps/building-app/modules/) (`prepare*`), [context and events](/serverless-apps/building-app/context/), [host actions](/serverless-apps/building-app/host-actions/) (`editor`, `project`, `profile`, `modal`, …) |
| `@crowdin/serverless-apps-sdk/react` | `AppUiProvider`, `useCrowdinContext`, `useCrowdinEvent`, `useTheme` |
| `@crowdin/serverless-apps-sdk/ui` | React [component library](/serverless-apps/building-app/user-interface/) styled to match the Crowdin UI (also re-exports `/react`) |
| `@crowdin/serverless-apps-sdk/ui/theme.css` | Tailwind theme source - for apps that run Tailwind CSS themselves (CLI-built apps do) |
| `@crowdin/serverless-apps-sdk/ui/styles.css` | Prebuilt stylesheet - for apps that do not use Tailwind |
| `@crowdin/serverless-apps-sdk/i18n` | [`AppI18nProvider`](/serverless-apps/building-app/i18n/) - Lingui-powered translations of your app’s own UI |
| `@crowdin/serverless-apps-sdk/api` | [`createCrowdinClient()`](/serverless-apps/building-app/crowdin-api/) - Crowdin REST API client executed by the host |
| `@crowdin/serverless-apps-sdk/storage` | [`createStorage()`](/serverless-apps/building-app/storage/) - Crowdin Storage, the app’s key-value records hosted by Crowdin |
| `@crowdin/serverless-apps-sdk/manifest.schema.json` | JSON Schema for [`manifest.json`](/serverless-apps/reference/manifest/) |
## Upgrading
[Section titled “Upgrading”](#upgrading)
The SDK and the [CLI](/serverless-apps/reference/cli-commands/) are released in lockstep - upgrade both together:
```bash
pnpm up @crowdin/serverless-apps-sdk @crowdin/serverless-apps-cli
```
New [module types](/serverless-apps/building-app/modules/), events typings, and manifest schema entries arrive with SDK releases, so an upgrade is how the app picks up new platform placements. After upgrading, `crowdin-serverless-apps manifest status` shows whether the stored manifest needs a push.
# Crowdin Storage
> Store a serverless app's data on the Crowdin side - shared, per-user, and module-scoped records
```ts
import { createStorage } from "@crowdin/serverless-apps-sdk/storage";
const { kv } = createStorage();
await kv.set("board:columns", ["To do", "In progress", "Done"]);
const columns = await kv.get("board:columns");
```
`createStorage()` returns the app’s storage hosted by Crowdin. Its `kv` namespace is a key-value space - one per installation, isolated from other apps and organizations on the server. The host executes the calls on behalf of the current viewer - no tokens ever reach the app. Not to be confused with the [Upload Storage API](/serverless-apps/building-app/crowdin-api/#uploading-files) (`client.uploadStorageApi`), which holds temporary files for REST API uploads.
Caution
Plain shared keys are readable by everyone who can use the app - in public projects that includes anonymous visitors - and writable by every signed-in user among them. Put anything user-specific under [`kv.user.*`](#per-user-records) and anything meant for a single surface under its [`module:` prefix](#module-scoped-records); the [access model](#access-model) has the full rules.
## Scopes
[Section titled “Scopes”](#scopes)
Storage access requires the `application.storage` scope in the [manifest](/serverless-apps/reference/manifest/):
manifest.json
```json
{
"scopes": ["application.storage"]
}
```
Without it every storage call is rejected.
## Reading and writing
[Section titled “Reading and writing”](#reading-and-writing)
```ts
await kv.set("greeting", { text: "hello" });
const value = await kv.get<{ text: string }>("greeting"); // undefined when the key does not exist
const entry = await kv.getWithMetadata<{ text: string }>("greeting"); // value + secret, createdAt, updatedAt, expiresAt
await kv.delete("greeting");
```
A value is any JSON value - string, number, boolean, array, or object. Keys are 1-500 characters of letters, digits, `:`, `.`, `_`, and `-`, case-sensitive; `:` is the conventional namespace separator (the [limits](#limits) table has the full constraints). Two key prefixes are reserved and carry special semantics - [`user:`](#per-user-records) and [`module:`](#module-scoped-records); any other key is a plain shared record. A key that starts with a reserved word but does not match the full shape - `user:{userId}:` with a numeric id, `module:{moduleKey}:` with a manifest module key - is rejected. A key is immutable - to rename a record, delete it and create a new one.
By default `set` overwrites an existing key. Pass `keyPolicy: "FAIL_IF_EXISTS"` to fail instead of overwriting when the key already exists:
```ts
import { createStorage, KeyExistsError } from "@crowdin/serverless-apps-sdk/storage";
try {
await kv.set("import:lock", Date.now(), { keyPolicy: "FAIL_IF_EXISTS" });
} catch (error) {
if (error instanceof KeyExistsError) {
// another member already started the import
}
}
```
## Listing
[Section titled “Listing”](#listing)
`list` returns the entries visible to the caller, 25 per page by default (500 max); page through with `offset`, or let `withFetchAll` collect every page. Without `orderBy` entries come in a stable insertion order (oldest first) - a stable base order for offset paging (records deleted or expiring mid-walk still shift later pages, as with any offset pagination). `orderBy` takes the REST sort format: `key`, `createdAt`, `updatedAt`, or `expiresAt`, each optionally followed by `desc`, comma-separated:
```ts
const page = await kv.list({ prefix: "board:", limit: 100 });
for (const { key, value } of page.data) {
console.log(key, value);
}
const freshest = await kv.list({ orderBy: "updatedAt desc,key" });
const everything = await kv.withFetchAll().list({ prefix: "board:" });
```
`withFetchAll` accepts an optional cap on the total number of entries, mirroring `@crowdin/crowdin-api-client`.
## Per-user records
[Section titled “Per-user records”](#per-user-records)
`kv.user.*` mirrors the whole surface but prefixes every key with `user:{userId}:` for the current user. Records under another user’s prefix do not exist for the caller - a direct `get` returns nothing, and listings skip them.
```ts
await kv.user.set("filters", { onlyMine: true });
const filters = await kv.user.get<{ onlyMine: boolean }>("filters");
const mine = await kv.user.list(); // only the current user's records
```
Unauthenticated viewers have no user identity, so `kv.user.*` throws for them while shared keys keep working - check `user.id` in the [context](/serverless-apps/building-app/context/) before offering per-user features.
## Module-scoped records
[Section titled “Module-scoped records”](#module-scoped-records)
A key under `module:{moduleKey}:` ties the record to the app [module](/serverless-apps/reference/manifest/#modules) with that manifest `key` and scopes it to that module’s surface: the record exists only where the app runs as that module, for viewers allowed to use it - so records of a managers-only module never reach the app’s other surfaces (the app’s installer is the one exception, see the [access model](#access-model)). Everywhere else listings skip the record and a direct `get` resolves `undefined`; `set` under a module key other than the current surface’s (or an unknown one) is rejected, while `delete` of a record outside the caller’s view resolves silently, like any missing key. Data the app’s modules share belongs in plain shared keys.
```ts
await kv.set("module:reviewer-panel:checklist", items);
```
There is no dedicated scope object for module records - write the prefix into the key itself.
TypeScript key hints
TypeScript suggests `module:` in every key position. Optionally enumerate the app’s module keys in the type parameter and the full prefixes get suggested instead - any other key stays accepted:
```ts
const { kv } = createStorage<"main" | "reports">();
await kv.set("module:reports:layout", layout); // key positions suggest module:main: and module:reports:
```
## Access model
[Section titled “Access model”](#access-model)
Who can reach a record follows where the app runs: the platform authorizes each call against the surface the app is running on, and every surface sees the shared records plus its own module’s records. On Crowdin Enterprise, surfaces are open to the organization’s members plus, for apps visible to guests, visitors of the organization’s crowdsourcing portals; on Crowdin (crowdin.com) they follow the app’s visibility settings in the installer’s projects. On both editions, when the app is visible to guests in a public project, that includes anonymous visitors: they read shared records without signing in, while any write requires a signed-in viewer. Treat shared records as effectively public - anyone the app is visible to can read them, and every signed-in user among them can change them. [Per-user records](#per-user-records) always require a signed-in viewer, and [module records](#module-scoped-records) exist only on their own surface. The app itself, calling with its own token over [direct REST access](#direct-rest-access), and the app’s installer, signed in on any of its surfaces, both see shared records and every module’s records - other users’ `user:` records stay private even from them. Anything sensitive belongs in `kv.user.*` - or under the `module:` prefix of a module only its team can open, when a whole team may hold it - with `secret: true` for tokens and credentials.
## Secrets
[Section titled “Secrets”](#secrets)
`secret: true` encrypts the value at rest:
```ts
await kv.user.set("gh-token", token, { secret: true });
```
The flag is fixed at creation - to change it, delete the key and create it again. Reads are transparent: the API returns the decrypted value to anyone who can read the record, in listings too. Encryption protects the stored value at rest; it does not hide the record from anyone who can read it under the [access model](#access-model) - so scope a secret to exactly the people who may know it. A personal credential lives in the `user:` space; a credential a whole team shares - an integration token, for example - lives under the [`module:` prefix](#module-scoped-records) of a module only that team can use:
```ts
await kv.set("module:cms-settings:api-token", token, { secret: true });
```
A plain shared key is never the place for a secret.
## Expiring records
[Section titled “Expiring records”](#expiring-records)
Pass `ttl` in seconds - from 60 up to 31536000 (one year) - to make a record expire:
```ts
await kv.set("cache:report", report, { ttl: 3600 });
```
`getWithMetadata` and `list` expose `expiresAt` (ISO 8601, `null` for permanent records). Passing `ttl` when overwriting restarts the countdown from that moment; omitting it keeps the existing expiry. An expired record behaves as deleted: `get` resolves `undefined` and listings skip it.
## Direct REST access
[Section titled “Direct REST access”](#direct-rest-access)
The same records are also reachable directly through the Crowdin REST API (`/api/v2/applications/{applicationIdentifier}/storage/kv/records`), authenticated with the app’s own access token carrying the `application.storage` scope. Personal access tokens are not accepted.
## Uninstall and data retention
[Section titled “Uninstall and data retention”](#uninstall-and-data-retention)
Uninstalling the app immediately deletes all of its records - shared and per-user alike, with no retention. A reinstall starts with an empty store. The same applies per module: removing a module from the manifest - or renaming its `key`, which replaces the module - immediately deletes that module’s records.
## Limits
[Section titled “Limits”](#limits)
| Limit | Value |
| --------------------- | -------------------------------------------------------------------------------------------------- |
| Key | 1-500 characters matching `^[a-zA-Z0-9:._-]+$`; case-sensitive; unique per installation; immutable |
| Value | any JSON value except `null` - up to 240 KiB serialized |
| `ttl` | optional, 60 seconds to 1 year (31536000 seconds) |
| Keys per installation | no quota - only the general API rate limits apply |
# UI Kit
> React components styled to match the Crowdin UI
The `@crowdin/serverless-apps-sdk/ui` entry point ships a React component library styled to match the Crowdin UI (it also re-exports everything from `/react`).
```tsx
import { AppUiProvider, Button, Card } from "@crowdin/serverless-apps-sdk/ui";
function App() {
return (
);
}
```
Wrap your app in [`AppUiProvider`](/serverless-apps/building-app/context/) so the components pick up the host theme: dark mode and Crowdin’s CSS variables are applied automatically. Like the rest of the SDK, this works only inside the Crowdin host - there is no standalone preview [outside it](/serverless-apps/development/troubleshooting/#opening-the-app-directly-in-a-browser).
## Components
[Section titled “Components”](#components)
The library is built on [shadcn/ui](https://ui.shadcn.com/docs/components) (the “new-york” style), re-themed with Crowdin’s design tokens. Each component keeps its upstream API, so the shadcn/ui docs are the reference for props, composition, and examples - import from `@crowdin/serverless-apps-sdk/ui` instead of your own `components/ui` folder.
The full set (\~55 components):
`accordion`, `alert`, `alert-dialog`, `aspect-ratio`, `avatar`, `badge`, `breadcrumb`, `button`, `button-group`, `card`, `carousel`, `chart`, `checkbox`, `collapsible`, `combobox`, `command`, `context-menu`, `dialog`, `direction`, `drawer`, `dropdown-menu`, `empty`, `field`, `form`, `hover-card`, `input`, `input-group`, `input-otp`, `item`, `kbd`, `label`, `menubar`, `native-select`, `navigation-menu`, `pagination`, `popover`, `progress`, `radio-group`, `resizable`, `scroll-area`, `select`, `separator`, `sheet`, `sidebar`, `skeleton`, `slider`, `sonner`, `spinner`, `switch`, `table`, `tabs`, `textarea`, `toggle`, `toggle-group`, `tooltip`
## Render errors
[Section titled “Render errors”](#render-errors)
`AppUiProvider` wraps its children in an error boundary. If the app throws while rendering, the boundary shows the error message in a themed alert instead of leaving a blank iframe. The boundary itself does not log - React reports every caught error to the app’s console on its own, so the error appears there exactly once.
To take over the presentation, render your own `AppErrorBoundary` closer to the failing subtree, optionally with a custom fallback:
```tsx
import { AppErrorBoundary } from "@crowdin/serverless-apps-sdk/ui";
Could not load: {error.message}
}>
```
The default fallback is deliberately not localized: a boundary that depends on i18n can fail for the same reason it is catching.
## Styles
[Section titled “Styles”](#styles)
Two stylesheets ship with the SDK - import exactly one of them:
| Import | When to use |
| -------------------------------------------- | ------------------------------------------------------------------------------------- |
| `@crowdin/serverless-apps-sdk/ui/theme.css` | Tailwind theme source - for apps that run Tailwind CSS themselves (CLI-built apps do) |
| `@crowdin/serverless-apps-sdk/ui/styles.css` | Prebuilt stylesheet - for apps that do not use Tailwind |
Apps scaffolded with the [CLI](/serverless-apps/reference/cli-commands/) use Tailwind CSS and import `theme.css` out of the box.
# CI and Non-Interactive Use
> Run the serverless apps CLI in CI pipelines and scripts
When stdout is not a terminal (or the `CI` environment variable is set), the CLI prints plain text and never prompts. Combine:
* [`CROWDIN_PERSONAL_TOKEN`](/serverless-apps/reference/configuration/) for authentication,
* the app link: `CROWDIN_APP_ID` in the app’s [`.env` file](/serverless-apps/reference/configuration/#the-apps-env-file) - the starters gitignore `.env`, so a fresh CI checkout is unlinked; run `link --app-id ` in the pipeline (or write the `.env` line yourself),
* `--yes` to confirm actions (e.g. `publish --yes`),
* `--json` / `--print` for machine-readable `list` output.
Publish from CI
```bash
export CROWDIN_PERSONAL_TOKEN=***
crowdin-serverless-apps link --app-id 123
crowdin-serverless-apps publish --yes
```
[`manifest validate`](/serverless-apps/reference/cli-commands/#manifest) is the exception: it needs no token and no app link, so it runs in a bare checkout and works as a pre-commit hook or a pull-request gate. It exits with code 1 when `manifest.json` is invalid or is not valid JSON.
Gate a pull request
```bash
crowdin-serverless-apps manifest validate
```
With Crowdin Enterprise, also set `CROWDIN_BASE_URL` (e.g. `https://.api.crowdin.com`) when authenticating with a personal token.
## Accessibility mode
[Section titled “Accessibility mode”](#accessibility-mode)
The `--lite` global flag forces the same minimal output in a regular terminal.
# Customization
> Customize the build of a serverless Crowdin app with your own Vite and Lingui configuration
The build needs no configuration: the entry is `src/index.tsx` (or `.ts`/`.jsx`/`.js`), the output is `dist/app.js`. This page covers overriding the build; for the CLI’s runtime knobs see [environment variables](/serverless-apps/reference/configuration/).
## Custom Vite configuration
[Section titled “Custom Vite configuration”](#custom-vite-configuration)
To customize the build, add a regular `vite.config.ts` to the app - the CLI picks it up.
Caution
Your `vite.config.ts` **replaces** the zero-config defaults (the React, Lingui, and Tailwind CSS plugins) rather than extending them - include the plugins your app needs in your own config.
The platform-required settings are still enforced on top and always win: the output stays a single `dist/app.js` (an IIFE bundle with CSS injected by JS), because `/app.js` is the contract the Crowdin host loads.
## Custom Lingui configuration
[Section titled “Custom Lingui configuration”](#custom-lingui-configuration)
Advanced i18n setups can add their own `lingui.config.ts`, which replaces the zero-config defaults. Keep the PO format and the `locales/{locale}` catalog layout so the CLI can compile the catalogs - see [translations](/serverless-apps/building-app/i18n/).
# How dev and publish work
> Bundle modes and the development-to-publishing lifecycle of a serverless Crowdin app
The manifest declares where Crowdin loads the app’s bundle from: `bundle.mode` is either `internal` (Crowdin serves the bundle you uploaded) or `external` (Crowdin loads it from a URL).
The two CLI commands pivot the app between these modes:
* [`dev`](/serverless-apps/reference/cli-commands/#dev) offers to point your app at `http://localhost:/`, so everything you edit shows up inside Crowdin instantly, hot module replacement included.
* [`publish`](/serverless-apps/reference/cli-commands/#publish) uploads `dist/bundle.zip` and offers to switch the app back to the Crowdin-served bundle.
Both update the app in Crowdin *and* your local `manifest.json` - a changed `manifest.json` after `dev` is expected. Pass `--yes` to apply without prompts (e.g. in scripts) or `--no-manifest-sync` to leave the bundle mode untouched.
## The bundle contract
[Section titled “The bundle contract”](#the-bundle-contract)
The Crowdin host always loads `/app.js`. The CLI’s build produces exactly that: a single `dist/app.js` (an IIFE bundle with CSS injected by JS), plus runtime assets such as translation catalogs under `dist/locales/`. During `dev`, the same contract is preserved - the dev server serves a generated `app.js` that enables hot module replacement inside the Crowdin iframe.
## Who can see the app
[Section titled “Who can see the app”](#who-can-see-the-app)
Publishing changes where the bundle is served from - it does not widen the audience. The app is registered and installed in your account when `create` runs and stays private to it until you [submit it to the Crowdin Store](https://support.crowdin.com/developer/crowdin-apps-publishing/) - serverless apps can be listed on the store and installed from it like any other Crowdin app.
In each account where the app is installed, who sees it is governed by the manifest’s [`default_permissions`](/serverless-apps/reference/manifest/#default_permissions) (defaults: `user: owner`, `project: own`) and by the [installed app’s settings](https://support.crowdin.com/developer/crowdin-apps-installation/), where an admin can change the audience later - e.g. open the app to managers or all project members.
To let teammates work on the app itself, share the repository: `.env` (the app link) is not committed, so they run `login` and `link` once - see [the .env file](/serverless-apps/reference/configuration/#the-apps-env-file). To remove an app you no longer need, uninstall it from the apps settings in Crowdin.
## Keeping the manifest in sync
[Section titled “Keeping the manifest in sync”](#keeping-the-manifest-in-sync)
`manifest.json` lives in your repository, and Crowdin stores its own copy for the registered app. Besides the automatic bundle-mode updates, the CLI gives you explicit sync commands:
* `manifest validate` checks the local file against the schema, offline (exits with code 1 when invalid);
* `manifest status` diffs the local file against Crowdin (exits with code 1 on drift);
* `manifest pull` overwrites local fields with Crowdin’s values;
* `manifest push` uploads local changes to Crowdin.
See the [manifest reference](/serverless-apps/reference/manifest/) for what the file contains.
# Troubleshooting
> What the Crowdin host shows when a serverless app fails to load, and how to fix it
When a serverless app can’t render, the Crowdin host replaces it with a diagnostic panel. Each panel maps to a specific failure:
| Panel | What happened |
| ------------------------------------ | ------------------------------------------------------------------------------------------- |
| *This app has no bundle yet* | The app has no JavaScript bundle to run - nothing was published and no dev server is linked |
| *We couldn’t load the app* | The bundle URL failed to load |
| *The app is taking too long to load* | The bundle didn’t respond within a few seconds |
| *The app didn’t start* | The bundle loaded but never registered the expected module |
## This app has no bundle yet
[Section titled “This app has no bundle yet”](#this-app-has-no-bundle-yet)
Run [`publish`](/serverless-apps/reference/cli-commands/#publish) to upload a bundle, or [`dev`](/serverless-apps/reference/cli-commands/#dev) and accept the switch to the external bundle mode so Crowdin loads the app from your machine.
## We couldn’t load the app
[Section titled “We couldn’t load the app”](#we-couldnt-load-the-app)
When the bundle URL points at `localhost`, check that the dev server is actually running on that port. Two browser-specific traps:
* **Chrome** blocks requests to local addresses unless the page is allowed local network access - check the permission prompt or site settings.
* **Safari** refuses plain-HTTP localhost requests from an HTTPS page, and the CLI dev server is HTTP-only - publish the bundle instead, or develop in another browser (a local HTTPS reverse proxy in front of the dev server also works).
For a published bundle, reload the page; if it persists, the uploaded bundle is broken - re-run `publish`.
## The app is taking too long to load
[Section titled “The app is taking too long to load”](#the-app-is-taking-too-long-to-load)
The bundle URL is reachable but slow to respond. With a dev server, check it is running and reachable on that address (the first compile after startup can also take a moment - reload once it settles). For a published bundle, check your network connection and reload the page.
## The app didn’t start
[Section titled “The app didn’t start”](#the-app-didnt-start)
The bundle executed, but the host waited and no module matching `context.app.type` and `key` was registered. Typical causes:
* **Registration ran too late.** `prepare*` calls must run [synchronously at the top level of the bundle](/serverless-apps/building-app/modules/) - not inside an async callback, a dynamic import, or an event handler.
* **Key mismatch.** With several modules of one type in the manifest, each registration must pass its manifest `key`: `prepareProjectMenu({ … }, "my-key")`.
* **Module type not registered at all.** Every module type declared in the manifest must have a matching `prepare*` call in the bundle - one bundle serves them all.
The browser console shows what the SDK dispatcher resolved (`[apps-sdk] no module registered for …`).
## The app started, then the content area shows an error (or nothing)
[Section titled “The app started, then the content area shows an error (or nothing)”](#the-app-started-then-the-content-area-shows-an-error-or-nothing)
Once a module registers, the host’s diagnostic panels are out of the picture - anything that fails from here on is the app’s own render path, and there is no host panel for it. The SDK reports these failures in two ways:
* **A throw during React rendering** is caught by the error boundary built into [`AppUiProvider`](/serverless-apps/building-app/user-interface/#render-errors): the app area shows the error message in an alert, and React logs the full error to the console.
* **A throw or rejection in the module’s `render()` itself** - before React mounts anything - is reported by the dispatcher: `[apps-sdk] module render() failed` in the console, plus a plain-text message in the app area (unless the app already drew something).
Both messages appear in the console of the app’s iframe, which is cross-origin to the hosting Crowdin page - in the browser DevTools, pick the app’s frame in the console context selector if you don’t see them.
## Opening the app directly in a browser
[Section titled “Opening the app directly in a browser”](#opening-the-app-directly-in-a-browser)
The bundle only runs inside the Crowdin host - opened directly (e.g. `http://localhost:8080/app.js` or your external URL), the SDK has no host bridge to talk to and throws. To see the app, open it inside Crowdin: [`preview`](/serverless-apps/reference/cli-commands/#preview) deep-links to the right page.
## Changes don’t show up
[Section titled “Changes don’t show up”](#changes-dont-show-up)
* Each `dev` server carries its own hot reload on its own port, so several can run at once. When port 8080 is taken, `dev` reports the port it moved to (`Port 8080 is in use, using 8081 instead.`) and points Crowdin at that one. With `--port` the port is exact: `dev` stops instead of moving, so nothing silently takes over where Crowdin loads the app from.
* Translation catalogs are compiled once when `dev` starts - restart the dev server after editing `.po` files.
* The local `manifest.json` and Crowdin’s copy can drift - `manifest status` shows the diff (and exits with code 1), `manifest push` / `manifest pull` [resolve it](/serverless-apps/development/dev-and-publish/#keeping-the-manifest-in-sync).
## The console shows Content Security Policy violations
[Section titled “The console shows Content Security Policy violations”](#the-console-shows-content-security-policy-violations)
The app iframe runs under a strict CSP. What it allows and forbids:
* **Allowed**: `fetch`/XHR/WebSocket requests to any HTTPS host (plus localhost during development) - calling third-party APIs directly is fine; images, media, and fonts from `https:`, `data:`, and `blob:` URLs.
* **Forbidden**: Web Workers (`worker-src 'none'` - breaks libraries that spawn workers, such as code editors or PDF renderers), nested iframes and embeds (`frame-src`/`child-src 'none'`), native form submissions (`form-action 'none'`), and scripts from anywhere but the app’s bundle.
A library that fails only inside Crowdin most likely hits one of these - the console’s CSP violation report names the blocked directive.
## API calls fail
[Section titled “API calls fail”](#api-calls-fail)
* Every REST call must be covered by the [`scopes`](/serverless-apps/reference/manifest/#scopes) declared in the manifest - calls outside them are rejected, and an empty `scopes` array means no API access at all. Note that `:write` does not imply read access.
* `FormData` bodies and GraphQL are not supported by the host bridge - see the [API limitations](/serverless-apps/building-app/crowdin-api/#limitations).
# Introduction
> What serverless Crowdin apps are and how the manifest, the CLI, and the SDK fit together
**Serverless apps** are [Crowdin apps](https://support.crowdin.com/developer/crowdin-apps-about/) without a backend: a static JavaScript bundle that runs right inside the Crowdin UI. Crowdin serves your bundle, injects the user and project context, and performs REST API calls on the current user’s behalf - restricted to the scopes your manifest declares. There is nothing for you to host or operate.
Serverless apps work in both [Crowdin](https://crowdin.com/) and [Crowdin Enterprise](https://crowdin.com/enterprise).
## How it fits together
[Section titled “How it fits together”](#how-it-fits-together)
A serverless app is made of three parts:
* **`manifest.json`** declares the app: its name, the UI modules it adds to Crowdin, the API scopes it may use, and where its bundle is served from.
* **The [CLI](/serverless-apps/reference/cli-commands/)** builds your frontend into a single `dist/app.js` (plus assets such as translation catalogs) and manages the app in Crowdin: while `dev` runs, Crowdin loads the bundle straight from your machine; after `publish`, Crowdin serves the uploaded bundle itself.
* **The [SDK](/serverless-apps/building-app/overview/)** runs inside the bundle: it registers your modules, exposes typed context, events, and host actions, and provides a Crowdin REST API client that needs no tokens, a UI kit styled to match Crowdin, and an i18n runtime for translating your app.
## When to build a serverless app
[Section titled “When to build a serverless app”](#when-to-build-a-serverless-app)
Serverless apps are the fastest way to extend the Crowdin UI: panels in the editor, project tools, dashboards, menus, and other frontend experiences that work with Crowdin data through the REST API.
If your app needs its own backend - webhooks, background jobs, file processing, integrations with external services under your own credentials - build a classic Crowdin app with the [Crowdin Apps SDK](https://crowdin.github.io/app-project-module/) instead.
## Packages
[Section titled “Packages”](#packages)
| Package | Description |
| -------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
| [`@crowdin/serverless-apps-cli`](https://www.npmjs.com/package/@crowdin/serverless-apps-cli) | Create, develop, preview, and publish serverless apps from your terminal |
| [`@crowdin/serverless-apps-sdk`](https://www.npmjs.com/package/@crowdin/serverless-apps-sdk) | Typed runtime SDK: host bridge, React hooks, UI components, i18n, and an in-app Crowdin API client |
Ready to try it? Head over to the [quick start](/serverless-apps/getting-started/quick-start/) - let your AI coding agent build the first app for you, or do it by hand.
# Quick Start
> Create your first serverless Crowdin app - let your AI coding agent build it, or run the CLI yourself
Create your first app in minutes: run the CLI walkthrough below, or install the [Crowdin plugin](#install-the-crowdin-plugin) and let your AI coding agent do it for you.
AI Assistance
[Open in Claude](https://claude.ai/new?q=Help%20me%20build%20a%20Crowdin%20app.%20Do%20the%20following%3A%0A1.%20Install%20the%20Crowdin%20plugin%20with%20%60npx%20plugins%20add%20crowdin%2Fskills%60.%0A2.%20Ask%20me%20what%20the%20app%20should%20do%2C%20then%20use%20the%20create-app%20skill%20to%20scaffold%2C%20develop%2C%20and%20publish%20it.%0A3.%20After%20publishing%2C%20open%20a%20preview%20so%20I%20can%20see%20the%20app%20inside%20Crowdin.)[Open in ChatGPT](https://chatgpt.com/?q=Help%20me%20build%20a%20Crowdin%20app.%20Do%20the%20following%3A%0A1.%20Install%20the%20Crowdin%20plugin%20with%20%60npx%20plugins%20add%20crowdin%2Fskills%60.%0A2.%20Ask%20me%20what%20the%20app%20should%20do%2C%20then%20use%20the%20create-app%20skill%20to%20scaffold%2C%20develop%2C%20and%20publish%20it.%0A3.%20After%20publishing%2C%20open%20a%20preview%20so%20I%20can%20see%20the%20app%20inside%20Crowdin.)[Open in Cursor](https://cursor.com/link/prompt?Help%20me%20build%20a%20Crowdin%20app.%20Do%20the%20following%3A%0A1.%20Install%20the%20Crowdin%20plugin%20with%20%60npx%20plugins%20add%20crowdin%2Fskills%60.%0A2.%20Ask%20me%20what%20the%20app%20should%20do%2C%20then%20use%20the%20create-app%20skill%20to%20scaffold%2C%20develop%2C%20and%20publish%20it.%0A3.%20After%20publishing%2C%20open%20a%20preview%20so%20I%20can%20see%20the%20app%20inside%20Crowdin.)
```
Help me build a Crowdin app. Do the following:
1. Install the Crowdin plugin with `npx plugins add crowdin/skills`.
2. Ask me what the app should do, then use the create-app skill to scaffold, develop, and publish it.
3. After publishing, open a preview so I can see the app inside Crowdin.
```
## Requirements
[Section titled “Requirements”](#requirements)
* Node.js `^22.19 || ^24` - 22.19 or newer within the 22.x line, or any 24.x
* A Node.js package manager - the examples use [pnpm](https://pnpm.io/installation), but npm or yarn work too
* A [Crowdin](https://crowdin.com/) or [Crowdin Enterprise](https://crowdin.com/enterprise) account
## Create your first app
[Section titled “Create your first app”](#create-your-first-app)
1. Install the CLI globally:
```bash
npm install --global @crowdin/serverless-apps-cli
```
Or run it ad hoc with `npx @crowdin/serverless-apps-cli `.
2. Sign in with your Crowdin account:
```bash
crowdin-serverless-apps login
```
The CLI opens the browser for sign-in and detects your Crowdin Enterprise organization automatically.
3. Scaffold an app from a starter template and register it in Crowdin:
```bash
crowdin-serverless-apps create my-app
cd my-app
pnpm install
```
`create` asks which of the [two starter templates](/serverless-apps/reference/cli-commands/#create) to use, scaffolds the project (`manifest.json`, `src/`, `public/`, `locales/`), registers the app in Crowdin, and links the folder to it via `.env`. Both templates live in the [serverless-apps-starter-kit](https://github.com/crowdin-community/serverless-apps-starter-kit) repository - browse them to see what a complete app looks like.
4. Develop live inside Crowdin, with hot reload:
```bash
crowdin-serverless-apps dev
```
Accept the prompt to point the app at your local server. While `dev` runs, your app inside Crowdin loads straight from your machine - everything you edit shows up instantly.
5. See the app inside Crowdin - in another terminal, run:
```bash
crowdin-serverless-apps preview
```
It opens the browser right at the app’s place in the Crowdin UI.
6. Publish when you are ready:
```bash
crowdin-serverless-apps publish
```
The CLI builds the app, uploads the bundle, and lets Crowdin serve it. Publishing does not change [who can see the app](/serverless-apps/development/dev-and-publish/#who-can-see-the-app).
## Install the Crowdin plugin
[Section titled “Install the Crowdin plugin”](#install-the-crowdin-plugin)
If you use an AI coding agent - Claude Code, Cursor, Codex, or another tool with a plugin system - install the Crowdin plugin. It gives your agent Crowdin skills and slash commands like `/create-app`, which runs the whole flow above for you: it asks what the app should do, then scaffolds, develops, and publishes it.
```bash
npx plugins add crowdin/skills
```
The plugin activates automatically. No configuration needed.
See the [crowdin/skills](https://github.com/crowdin/skills) repository for the full list of skills and installation options for other tools.
## Where to go next
[Section titled “Where to go next”](#where-to-go-next)
* Learn [how `dev` and `publish` work](/serverless-apps/development/dev-and-publish/) under the hood.
* Browse the [CLI commands](/serverless-apps/reference/cli-commands/).
* Explore what the [SDK](/serverless-apps/building-app/overview/) gives your app at runtime.
* See the full source of the [starter templates](https://github.com/crowdin-community/serverless-apps-starter-kit) on GitHub.
# CLI Commands
> Reference of all crowdin-serverless-apps CLI commands
[`@crowdin/serverless-apps-cli`](https://www.npmjs.com/package/@crowdin/serverless-apps-cli) is the command-line tool for serverless Crowdin apps: one binary scaffolds an app, runs it live inside Crowdin with hot reload, extracts and compiles translations, lints and formats your code, and publishes the result. It is a zero-config toolchain - React, Tailwind CSS, and Lingui i18n work out of the box, with Vite powering the dev server and the production build.
## Installation
[Section titled “Installation”](#installation)
Requires Node.js `^22.19 || ^24` - 22.19 or newer within the 22.x line, or any 24.x.
```bash
npm install --global @crowdin/serverless-apps-cli
```
Or run it ad hoc with `npx @crowdin/serverless-apps-cli `.
Run `crowdin-serverless-apps --help` for all options and examples.
## Develop
[Section titled “Develop”](#develop)
### create
[Section titled “create”](#create)
```bash
crowdin-serverless-apps create [name] [--template ] [--yes]
```
Create a new serverless app from a starter template, register it in Crowdin, and link the folder to it (`CROWDIN_APP_ID` in `.env`). Both templates scaffold the same sample app (`manifest.json`, `src/`, `public/`, `locales/`) and differ in who owns the build:
* [`projects-dashboard-cli`](https://github.com/crowdin-community/serverless-apps-starter-kit/tree/main/templates/projects-dashboard-cli) - the CLI handles building for you: `dev`, `build`, and `publish` work out of the box with the zero-config toolchain. The default (used with `--yes`).
* [`projects-dashboard-standalone`](https://github.com/crowdin-community/serverless-apps-starter-kit/tree/main/templates/projects-dashboard-standalone) - you control the build setup: the template ships its own Vite/Lingui configuration and build scripts; use the CLI for app management (`login`, `link`, `manifest`, `publish`).
### dev
[Section titled “dev”](#dev)
```bash
crowdin-serverless-apps dev [--port ] [--yes] [--no-manifest-sync]
```
Start a local dev server with hot reload; while it runs, your app inside Crowdin loads straight from your machine. See [how dev and publish work](/serverless-apps/development/dev-and-publish/).
The server listens on port 8080 (`PORT` sets a different starting point). If that port is taken - typically by a dev server for another app - `dev` reports the port it moved to and points Crowdin at that one. `--port ` is exact: `dev` stops rather than moving, so a scripted port never changes under you.
### preview
[Section titled “preview”](#preview)
```bash
crowdin-serverless-apps preview [--module ] [--project ]
```
Open the app’s page in Crowdin in the browser (does not start a server). Module types that have no page of their own (for example `modal` or `chat`) can’t be opened - the CLI explains why instead of opening the browser.
[Project modules](/serverless-apps/building-app/modules/) and `editor-right-panel` open inside a project, so they need one. `--project` accepts either the numeric id or the project identifier. In an interactive terminal the CLI lists the projects you can access and lets you pick one; without a TTY it falls back to the first project on that list, so a scripted `preview` still ends on a URL - the `Opening ` line names the project it used. Pass `--project` when a specific one matters, since the first project you can access is not necessarily one where the app has anything to show. If the account has no accessible projects, `preview` stops and says so.
## Build & publish
[Section titled “Build & publish”](#build--publish)
### build
[Section titled “build”](#build)
```bash
crowdin-serverless-apps build [--no-extract] [--no-bundle]
```
Build the app and package it for publishing (`dist/bundle.zip`).
### extract
[Section titled “extract”](#extract)
```bash
crowdin-serverless-apps extract
```
Scan the source for translatable text and update the `locales/*.po` catalogs. See [translations](/serverless-apps/building-app/i18n/).
### publish
[Section titled “publish”](#publish)
```bash
crowdin-serverless-apps publish [--no-build] [--yes] [--no-manifest-sync]
```
Build, upload the bundle to Crowdin, and switch the app to serve it.
## Quality
[Section titled “Quality”](#quality)
### lint
[Section titled “lint”](#lint)
```bash
crowdin-serverless-apps lint
```
Check the app’s code for problems (Biome, no configuration needed).
### format
[Section titled “format”](#format)
```bash
crowdin-serverless-apps format
```
Auto-format the app’s code.
## Manage apps
[Section titled “Manage apps”](#manage-apps)
### list
[Section titled “list”](#list)
```bash
crowdin-serverless-apps list [--print] [--json]
```
List your serverless apps. Alias: `ls`.
### link
[Section titled “link”](#link)
```bash
crowdin-serverless-apps link [--app-id ]
```
Link the local project to an existing app.
### manifest
[Section titled “manifest”](#manifest)
```bash
crowdin-serverless-apps manifest validate
crowdin-serverless-apps manifest status
crowdin-serverless-apps manifest pull
crowdin-serverless-apps manifest push
```
Check, diff, pull, or push `manifest.json`.
`manifest validate` checks the file against the [manifest schema](/serverless-apps/reference/manifest/) and needs no login and no app link - it never contacts Crowdin. It exits with code 1 when the manifest is invalid or is not valid JSON, which makes it usable as a pre-commit or CI gate.
`manifest status` exits with code 1 when the local file drifts from Crowdin.
## Account
[Section titled “Account”](#account)
### login
[Section titled “login”](#login)
```bash
crowdin-serverless-apps login [--file-storage] [--port ]
```
Sign in via the browser; tokens are stored in the OS keychain, or in an encrypted file with `--file-storage`. Your Crowdin Enterprise organization is detected automatically.
`--port` pins the browser-callback port to one of `26140`-`26144` (useful for SSH port forwarding); by default the first free one of those is used.
### logout
[Section titled “logout”](#logout)
```bash
crowdin-serverless-apps logout
```
Log out and remove the stored token.
# Environment Variables
> Environment variables and the .env file recognized by the serverless apps CLI
This page covers the runtime knobs of the CLI. Looking to customize the build (Vite, Lingui)? See [customization](/serverless-apps/development/customization/).
## Environment variables
[Section titled “Environment variables”](#environment-variables)
| Variable | Purpose |
| ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `CROWDIN_PERSONAL_TOKEN` | Authenticate with a personal access token instead of `login` - useful in CI |
| `CROWDIN_BASE_URL` | Crowdin API base URL, e.g. `https://.api.crowdin.com` - set it when using a personal token with Crowdin Enterprise |
| `PORT` | Port `dev` starts from (8080 when unset) - if it is taken, `dev` moves to the next free one, while `--port` is exact |
| `CROWDIN_DEV_CORS_ORIGIN` | Extra origins allowed to fetch from the dev server |
| `CROWDIN_TEMPLATES_REPO` | GitHub `owner/name` repository `create` fetches starter templates from (default [`crowdin-community/serverless-apps-starter-kit`](https://github.com/crowdin-community/serverless-apps-starter-kit)) - useful for forks |
| `CROWDIN_TEMPLATES_REF` | Branch or tag of the templates repository (default `main`) |
`login` detects your Crowdin Enterprise organization automatically - no extra configuration needed.
Note
Credentials stored by `login` take precedence over `CROWDIN_PERSONAL_TOKEN` - on a machine where you have signed in, the token is ignored. Run `logout` first to force the personal token.
## The app’s .env file
[Section titled “The app’s .env file”](#the-apps-env-file)
`CROWDIN_APP_ID` identifies the app the folder is linked to. It lives in the app’s `.env` **file**, where `create` and `link` write it - the CLI reads it from that file only, so setting it as a shell or CI environment variable has no effect.
`.env` is gitignored in the starter templates, so it does not travel with the repository: teammates cloning the app (and [CI checkouts](/serverless-apps/development/ci/)) need to run `crowdin-serverless-apps login` and `link` once before `dev` or `publish` work.
# Manifest
> Full reference of the manifest.json file that describes a serverless Crowdin app
`manifest.json` describes the app. Point `$schema` at the bundled JSON Schema to get validation and autocomplete in your editor:
manifest.json
```json
{
"$schema": "./node_modules/@crowdin/serverless-apps-sdk/manifest.schema.json",
"name": "My App",
"bundle": { "mode": "internal" },
"scopes": ["project:read"],
"modules": {
"project-menu": [{ "key": "menu", "name": "My App" }]
}
}
```
Note
Classic Crowdin app manifest fields such as `baseUrl`, `identifier`, and per-module `url` are not allowed - serverless apps have no backend. The schema rejects them.
## Top-level fields
[Section titled “Top-level fields”](#top-level-fields)
| Field | Required | Description |
| ---------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `name` | yes | Human-readable app name (2-255 characters) shown in Crowdin |
| `bundle` | yes | Where Crowdin loads the bundle from - see [bundle](#bundle) |
| `scopes` | yes | What the [in-app API client](/serverless-apps/building-app/crowdin-api/) may do on the user’s behalf - see [scopes](#scopes) |
| `modules` | yes | The UI modules the app adds to Crowdin, keyed by module type - see [modules](#modules) |
| `description` | no | App description |
| `logo` | no | Logo path served from the bundle root; a root-relative path beginning with `/`, e.g. `/logo.svg` for `public/logo.svg` |
| `stringBasedAvailable` | no | Whether the app is offered in string-based projects in addition to file-based ones (default `false`) |
| `default_permissions` | no | Default audience after installation - see [default\_permissions](#default_permissions) |
### bundle
[Section titled “bundle”](#bundle)
`bundle.mode` is `internal` (Crowdin serves the bundle you uploaded with `publish`) or `external` (Crowdin loads it from `bundle.url` - your own hosting or the local dev server). With `external`, `url` is required and points at a folder: the host appends `app.js` to it.
This field is managed for you by the CLI’s `dev` and `publish` commands - see [how dev and publish work](/serverless-apps/development/dev-and-publish/).
### scopes
[Section titled “scopes”](#scopes)
OAuth-style scopes the app may exercise against the Crowdin REST API on behalf of the current user; calls outside these scopes are rejected. An empty array means no API access.
See [understanding scopes](https://support.crowdin.com/developer/understanding-scopes/) for the list - e.g. `project`, `project.translation`, `tm`, `glossary`. Narrow a scope with the `:read` or `:write` postfix (e.g. `project:read`); note that `:write` does not imply read access.
[Crowdin Storage](/serverless-apps/building-app/storage/) requires the `application.storage` scope.
### default\_permissions
[Section titled “default\_permissions”](#default_permissions)
Who the app is available to right after installation (the admin can change it later - see [app installation](https://support.crowdin.com/developer/crowdin-apps-installation/)):
| Key | Values | Default |
| --------- | ------------------------------------ | ------- |
| `user` | `owner`, `managers`, `all`, `guests` | `owner` |
| `project` | `own`, `restricted` | `own` |
## Modules
[Section titled “Modules”](#modules)
Every entry under `modules` is a list, keyed by [module type](/serverless-apps/building-app/modules/). The schema enumerates the module types the installed SDK version supports - new types arrive with SDK releases. All modules share three core fields:
| Field | Required | Description |
| ------ | -------- | -------------------------------------------------------------------------------------------------------------------- |
| `key` | yes | Unique within the app (3-255 characters; letters, digits, `_`, `-`); disambiguates multiple modules of the same type |
| `name` | yes | Human-readable module name (3-255 characters) |
| `logo` | no | Logo path served from the bundle root, e.g. `/logo.svg` |
Some module types require extra fields:
| Module type | Extra required fields |
| ----------------------------------------------------------------------------- | --------------------- |
| `editor-right-panel`, `editor-translations-panel`, `editor-background-worker` | `modes` |
| `editor-asset-panel` | `fileNamePattern` |
| `context-menu` | `options` |
| all other types | none |
### modes
[Section titled “modes”](#modes)
Editor modules declare the editor modes and views they appear in - a unique, non-empty subset of:
`translate`, `review`, `assets`, `comfortable`, `side-by-side`, `multilingual`
### fileNamePattern
[Section titled “fileNamePattern”](#filenamepattern)
Asset-panel modules declare which asset files they handle with a wildcard pattern, e.g. `"*.psd"`.
### options (context-menu)
[Section titled “options (context-menu)”](#options-context-menu)
Context-menu entries declare where they appear and what they do (see the [context menu module docs](https://support.crowdin.com/developer/crowdin-apps-module-context-menu/) for how the entries look in the Crowdin UI):
| Field | Required | Description |
| ---------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `location` | yes | Which Crowdin context menu the entry is added to: `tm`, `glossary`, `language`, `screenshot`, `style_guide`, `source_file`, `translated_file` |
| `type` | yes | What activating the entry does: `modal` opens one of the app’s modal modules, `redirect` jumps to one of the app’s menu modules |
| `module` | yes | The target module as a single `{"": ""}` pair, e.g. `{"modal": "details"}`. `modal` entries always use the `modal` type; `redirect` entries use a menu module type allowed for the location: `organization-menu` (Crowdin Enterprise) or `profile-resources-menu` (crowdin.com) for `tm`, `glossary` and `style_guide`; `project-tools`, `project-reports`, `project-menu` or `project-integrations` for the rest |
### Example with several module types
[Section titled “Example with several module types”](#example-with-several-module-types)
manifest.json
```json
{
"$schema": "./node_modules/@crowdin/serverless-apps-sdk/manifest.schema.json",
"name": "My App",
"description": "Editor panel and TM context menu",
"logo": "/logo.svg",
"bundle": { "mode": "internal" },
"scopes": ["project:read", "tm:read"],
"modules": {
"editor-right-panel": [
{ "key": "panel", "name": "My Panel", "modes": ["translate", "review"] }
],
"modal": [{ "key": "details", "name": "Details" }],
"context-menu": [
{
"key": "tm-action",
"name": "Open in My App",
"options": { "location": "tm", "type": "modal", "module": { "modal": "details" } }
}
]
}
}
```
## Checking the manifest
[Section titled “Checking the manifest”](#checking-the-manifest)
`manifest validate` checks the local file against the same schema, without contacting Crowdin:
```bash
crowdin-serverless-apps manifest validate
```
It needs no login and no app link, exits with code 1 when the manifest is invalid, and reports each problem with the path to fix. Crowdin still validates server-side when the manifest is pushed; this is the fast local check. Keep `$schema` in the file so your editor flags mistakes as you type.
## Syncing with Crowdin
[Section titled “Syncing with Crowdin”](#syncing-with-crowdin)
Crowdin stores its own copy of the manifest for the registered app. Use the CLI’s [`manifest status` / `pull` / `push`](/serverless-apps/reference/cli-commands/#manifest) commands to diff and sync it with your local file.