# Deployment

You can deploy your Crowdin app to any hosting provider that supports Node.js. Below you can find examples of how to deploy your Crowdin app to Vercel, AWS Lambda, and Cloudflare Workers.

## Vercel

[Vercel](https://vercel.com) can deploy your Crowdin app from existing repo (GitHub, Gitlab, Bitbucket).

### Vercel Configuration

Go to Vercel and proceed with next steps:

- Create a new project.
- Under the `Environment Variables` section, define following variables:
  - `CLIENT_ID` - Crowdin App Client ID.
  - `CLIENT_SECRET` - Crowdin App Client Secret.
- Add Postgres SQL Storage.
- Storage -> Connect Store -> Create New -> Select Postgres.
- Deploy.

### Example of the Crowdin app

Project structure:

- root
    - index.js
    - logo.png
    - package.json
    - package-lock.json
    - vercel.json
    - .gitignore
App configuration:

```js title="index.js"
import crowdinModule from '@crowdin/app-project-module';

crowdinModule.createApp({
  baseUrl: `https://${process.env.VERCEL_URL}`,
  clientId: process.env.CLIENT_ID,
  clientSecret: process.env.CLIENT_SECRET,
  name: 'Sample App',
  identifier: 'sample-app',
  description: 'Sample App description',
  postgreConfig: {
    host: process.env.POSTGRES_HOST,
    user: process.env.POSTGRES_USER,
    password: process.env.POSTGRES_PASSWORD,
    database: process.env.POSTGRES_DATABASE,
    ssl: true
  },
  imagePath: import.meta.dirname + '/logo.png',
  customMT: [
    {
      translate: () => {
        console.log('translate');
      },
      validate: () => {
        console.log('validate');
      }
    }
  ]
});
```

```json title="vercel.json"
{
  "version": 2,
  "builds": [
    {
      "src": "./index.js",
      "use": "@vercel/node"
    }
  ],
  "routes": [
    {
      "src": "/(.*)",
      "dest": "/"
    }
  ]
}
```

```txt title=".gitignore"
/node_modules/
```

```json title="package.json"
{
  "name": "crowdin-app-demo",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "type": "module",
  "engines": {
    "node": "20.x"
  },
  "scripts": {
    "start": "node index.js"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@crowdin/app-project-module": "^1.0.0"
  }
}
```

## AWS Lambda

You can use [Serverless](https://github.com/serverless/serverless) to deploy your Crowdin app to AWS Lambda.

### Example of the Crowdin app

It uses Serverless framework so in order to deploy it to AWS you just need to install deps `npm i` and after run this command `serverless deploy`.

In this example we are using PostgreSQL as a storage (e.g. AWS `RDS` with PostgreSQL engine can be used). Visit the [Database](/app-project-module/database/) page to see other options.

**Caution:** There is no way to return binary data (e.g. png), so the solution here would be to use the `svg` logo.

Project structure:

- root
    - handler.js
    - logo.svg
    - package.json
    - env.json
    - package-lock.json
    - serverless.yml
    - .gitignore
```js title="handler.js"
const serverless = require('serverless-http');
const path = require('path');
const express = require('express');
const app = express();
const crowdinApp = require('@crowdin/app-project-module');

crowdinApp.addCrowdinEndpoints(app, {
  baseUrl: process.env.LAMBDA_URL,
  clientId: process.env.CLIENT_ID,
  clientSecret: process.env.CLIENT_SECRET,
  name: 'Sample App',
  identifier: 'sample-app',
  description: 'Sample App description',
  postgreConfig: {
    user: process.env.PG_USER,
    password: process.env.PG_PASSWORD,
    database: process.env.PG_DB,
    host: process.env.PG_HOST
  },
  imagePath: path.join(__dirname, 'logo.svg'),
  projectIntegration: {
    getIntegrationFiles: async () => {
      return [
        {
          id: '12',
          name: 'File from integration',
          type: 'json',
          parentId: '10'
        },
        {
          id: '14',
          name: 'File from integration 2',
          type: 'xml',
          parentId: '11'
        },
        {
          id: '10',
          name: 'Folder from integration'
        },
        {
          id: '11',
          name: 'Folder from integration 2'
        }
      ];
    },
    updateCrowdin: async () => {
      //TODO implement
    },
    updateIntegration: async () => {
      //TODO implement
    }
  }
});

module.exports.handler = serverless(app);
```

**Note:** This example uses CommonJS because the Serverless Framework and AWS Lambda treat `.js` files as CommonJS by default. To write the handler in ESM instead, add `"type": "module"` to the `package.json` below and export the handler with `export const handler = serverless(app);`.

```yml title="serverless.yml"
service: aws-node-express-api
frameworkVersion: '3'

provider:
  name: aws
  runtime: nodejs20.x

functions:
  api:
    handler: handler.handler
    environment: ${file(env.json)}
    url: true
```

```json title="env.json"
{
  "LAMBDA_URL": "https://<id>.execute-api.us-east-1.amazonaws.com",
  "CLIENT_ID": "<client-id>",
  "CLIENT_SECRET": "<client-secret>",
  "PG_HOST": "<pg_host>",
  "PG_USER": "<pg_user>",
  "PG_PASSWORD": "<pg_pass>",
  "PG_DB": "<pg_db>"
}
```

```txt title=".gitignore"
node_modules
.serverless
```

```json title="package.json"
{
  "name": "crowdin-serverless",
  "version": "1.0.0",
  "description": "",
  "dependencies": {
    "express": "^5.0.0",
    "serverless-http": "^4.0.0",
    "@crowdin/app-project-module": "^1.0.0"
  },
  "author": "",
  "license": "ISC"
}
```

**Caution:** This module requires Express 5, so use `serverless-http@^4.0.0`. Earlier versions are incompatible with Express 5.

## Cloudflare Workers

Deploy your Crowdin app to Cloudflare Workers with D1 database, KV storage for files, and static assets support.

**Prerequisites:** [Wrangler CLI](https://developers.cloudflare.com/workers/wrangler/), Cloudflare account

**Setup:**
1. Create D1 database: `npx wrangler d1 create crowdin-app-db`
2. Create KV namespace: `npx wrangler kv namespace create KVStore`
3. Update `wrangler.jsonc` with the generated IDs

### Project Structure

- root
    - worker/
      - index.ts
      - app.ts
    - public/
      - logo.svg
    - package.json
    - wrangler.jsonc
    - .gitignore
### Configuration Files

```json title="wrangler.jsonc"
{
  "$schema": "node_modules/wrangler/config-schema.json",
  "name": "crowdin-app",
  "main": "worker/index.ts",
  "compatibility_date": "2025-01-01",
  "compatibility_flags": [
    "nodejs_compat",
    "enable_nodejs_http_modules",
    "enable_nodejs_http_server_modules"
  ],
  "assets": {
    "binding": "ASSETS",
    "run_worker_first": ["/*", "!/@*", "!/src/*", "!/node_modules/*"]
  },
  "d1_databases": [
    {
      "binding": "DB",
      "database_name": "crowdin-app-db",
      "database_id": "your-d1-database-id"
    }
  ],
  "kv_namespaces": [
    {
      "binding": "KVStore",
      "id": "your-kv-namespace-id"
    }
  ],
  "triggers": {
    "crons": ["0 * * * *"]
  },
  "vars": {
    "CROWDIN_CLIENT_ID": "your-client-id",
    "CROWDIN_CLIENT_SECRET": "your-client-secret"
  }
}
```

```ts title="worker/index.ts"
import { httpServerHandler } from 'cloudflare:node';
import { createApp } from './app';
import * as crowdinModule from '@crowdin/app-project-module';
import type { Cron } from '@crowdin/app-project-module';
import type { Server } from 'node:http';

// Close previous server on HMR reload
const previousServer = (globalThis as any).__previousServer as Server | undefined;
if (previousServer) {
  previousServer.close();
}

// Module-scoped state
let handler: ExportedHandler | undefined;
let appInstance: ReturnType<typeof createApp> | undefined;

class CloudflareCron implements Cron {
  private handlers: Map<string, Array<() => Promise<void>>> = new Map();

  schedule(expression: string, task: () => Promise<void>): void {
    if (!this.handlers.has(expression)) {
      this.handlers.set(expression, []);
    }
    this.handlers.get(expression)?.push(task);
  }

  async runScheduled(expression: string): Promise<void> {
    const tasks = this.handlers.get(expression) || [];
    await Promise.allSettled(tasks.map(task => task()));
  }
}

const cron = new CloudflareCron();

function initializeApp(env: CloudflareEnv) {
  if (appInstance) {
    return appInstance;
  }

  const app = crowdinModule.express();

  const crowdinApp = createApp({
    app,
    clientId: env.CROWDIN_CLIENT_ID,
    clientSecret: env.CROWDIN_CLIENT_SECRET,
    assetsConfig: {
      fetcher: env.ASSETS,
    },
    d1Config: {
      database: env.DB,
    },
    fileStore: {
      getFile: async (fileId: string): Promise<Buffer> => {
        const data = await env.KVStore.get(fileId, 'arrayBuffer');
        if (!data) {
          throw new Error(`File not found: ${fileId}`);
        }
        return Buffer.from(data);
      },
      storeFile: async (content: Buffer): Promise<string> => {
        const fileId = `file_${crypto.randomUUID()}`;
        await env.KVStore.put(fileId, content, { expirationTtl: 86400 });
        return fileId;
      },
      deleteFile: async (fileId: string): Promise<void> => {
        await env.KVStore.delete(fileId);
      }
    },
    cron
  });

  appInstance = crowdinApp;
  return appInstance;
}

function initializeHandler(env: CloudflareEnv): ExportedHandler {
  if (handler) {
    return handler;
  }

  const { app } = initializeApp(env);
  const port: number = 3000;
  (globalThis as any).__previousServer = app.listen(port);
  handler = httpServerHandler({ port });

  return handler;
}

export default {
  async fetch(request: Request, env: CloudflareEnv, ctx: ExecutionContext) {
    return initializeHandler(env).fetch!(request, env, ctx);
  },
  async scheduled(controller: ScheduledController, env: CloudflareEnv, ctx: ExecutionContext) {
    console.log(`Cron triggered: ${controller.cron}`);

    try {
      initializeApp(env);
      await cron.runScheduled(controller.cron);
    } catch (error) {
      console.error('Error executing scheduled tasks:', error);
    }
  }
};
```

```ts title="worker/app.ts"
import * as crowdinModule from '@crowdin/app-project-module';
import type { AssetsConfig, FileStore, Cron, ClientConfig, D1StorageConfig } from '@crowdin/app-project-module';

export function createApp({
  app,
  clientId,
  clientSecret,
  assetsConfig,
  d1Config,
  fileStore,
  cron
}: {
  app: ReturnType<typeof crowdinModule.express>;
  clientId: string;
  clientSecret: string;
  assetsConfig: AssetsConfig;
  d1Config: D1StorageConfig;
  fileStore: FileStore;
  cron: Cron;
}) {
  const configuration: ClientConfig = {
    name: "My Crowdin App",
    identifier: "my-crowdin-app",
    description: "Crowdin app deployed on Cloudflare Workers",
    clientId,
    clientSecret,
    disableLogsFormatter: true,
    assetsConfig,
    d1Config,
    fileStore,
    cron,
    imagePath: '/logo.svg',
    assetsPath: '/assets',

    // Add your module configurations here
    // Example: customMT, projectIntegration, etc.
  };

  const crowdinApp = crowdinModule.addCrowdinEndpoints(app, configuration);

  return { app, crowdinApp };
}
```

```json title="package.json"
{
  "name": "crowdin-app",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "dev": "wrangler dev",
    "deploy": "wrangler deploy",
    "cf-typegen": "wrangler types --env-interface CloudflareEnv"
  },
  "dependencies": {
    "@crowdin/app-project-module": "^1.0.0"
  },
  "devDependencies": {
    "@cloudflare/workers-types": "^4.0.0",
    "wrangler": "^4.0.0"
  }
}
```

```txt title=".gitignore"
node_modules
.wrangler
.dev.vars
```

**Deploy:**
1. Install dependencies: `npm install`
2. Generate types: `npm run cf-typegen`
3. Deploy: `npm run deploy`

**Cron:** Configure scheduled events in `wrangler.jsonc` under `triggers.crons` using standard cron syntax.

**Assets:** Static files from the `public/` directory are automatically served via Cloudflare's edge network using the `ASSETS` binding.