---
title: Technical integrations
description: Every outside service the Leitagu apps and their hosting depend on, what each is used for, which app uses it, its settings, endpoints, code and what happens when it fails.
section: platform
order: 8
updated: 2026-10-23
---

# Technical integrations

Leitagu depends on few outside services. The apps talk to Anthropic (Claude, optional), Resend (account emails), Migadu (the CRM's shared mailbox) and the stores' own shops (Shopify, WooCommerce, feeds). The hosting uses GitHub Actions and GitHub Container Registry to build, Coolify on a DigitalOcean droplet to run, and Cloudflare R2 for backups; the domain and its DNS are at zone.ee. There is no outside analytics, maps, payment, SMS or file storage service. This page lists each integration as the code shows it; settings are named, never their values.

## Summary

| Integration | What Leitagu uses it for | App | Settings |
| --- | --- | --- | --- |
| [Anthropic (Claude)](#anthropic-claude) | Reading typed chat messages and room photos | Shop | `ANTHROPIC_API_KEY` or `ANTHROPIC_AUTH_TOKEN` |
| [Resend](#resend) | Verification, password-reset and staff invite emails | Shop, admin | `RESEND_API_KEY`, `EMAIL_FROM`, `EMAIL_REPLY_TO`, `APP_URL` |
| [Migadu](#migadu) | The CRM's shared partner mailbox (IMAP and SMTP) | CRM | `CRM_MAIL_ADDRESS`, `CRM_MAIL_PASSWORD`, `CRM_MAIL_NAME`, `CRM_IMAP_HOST`, `CRM_IMAP_PORT`, `CRM_SMTP_HOST`, `CRM_SMTP_PORT`, `CRON_SECRET` |
| [Shopify](#shopify) | Importing a store's products | Partner (and admin MCP tools) | `INTEGRATION_SECRET_KEY`, `SHOPIFY_API_VERSION`, `CRON_SECRET` |
| [WooCommerce](#woocommerce) | Importing a store's products | Partner (and admin MCP tools) | `CRON_SECRET` |
| [Product feeds and CSV](#product-feeds-and-csv) | Importing a store's products | Partner (and admin MCP tools) | `CRON_SECRET` |
| [Store websites](#store-websites-sign-up-proof) | Proving a store sign-up with a meta tag | Partner | None |
| [AI assistant clients](#ai-assistant-clients-mcp) | Claude and other MCP clients connecting to the staff tools | Admin | `ADMIN_URL` |
| [Estonian business register](#estonian-business-register) | A link from a CRM company to its register entry | CRM | None |
| [Coolify on DigitalOcean](#coolify-on-digitalocean) | Running the apps and the database | All | `COOLIFY_DEPLOY_TOKEN` (GitHub secret) |
| [GitHub Actions and ghcr.io](#github-actions-and-ghcrio) | Building, checking and storing the app images | All | `GITHUB_TOKEN` (built in) |
| [Cloudflare R2](#cloudflare-r2) | Off-site backups | Database, Coolify | Kept in Coolify, not in the repository |
| [zone.ee (domain and DNS)](#zoneee-domain-and-dns) | Registering `leitagu.ee` and its DNS records | All | None |

`CRON_SECRET` protects Leitagu's own scheduled endpoints, not an outside service; it is listed where an integration is driven by one.

## Services the apps call

### Anthropic (Claude)

- **What.** Anthropic's API, through the official SDK (`@anthropic-ai/sdk`).
- **Used for.** Turning a shopper's typed message into search parameters, and reading a room photo (size, colours, openings and furniture) to build a room. Search, ranking and replies are Leitagu's own code; Claude only reads the input. See [How the shop answers a chat](architecture.md#how-the-shop-answers-a-chat).
- **App.** Shop (`apps/web`).
- **Settings.** `ANTHROPIC_API_KEY`, or `ANTHROPIC_AUTH_TOKEN`. Optional: without either, Claude is off.
- **Code.** `apps/web/src/lib/server/llm.ts` (the model and the prompts), used by the chat page and `POST /api/rooms/photo` (`apps/web/src/app/api/rooms/photo/route.ts`).
- **When it fails.** Without a key, chat messages are read by the keyword parser (`packages/core/src/shared/parse.ts`) and the photo route answers 503 (`photo_needs_key`), so the shop offers a room template instead. An API error or a refusal also falls back to the keyword parser; a photo that can't be read answers 422 (`photo_fail`). Photos are limited to 20 per person per hour, counted in memory per server.

### Resend

- **What.** Resend's email API, called directly at `https://api.resend.com/emails` (no SDK).
- **Used for.** Email verification and password-reset emails for shoppers, and staff invite emails, in Estonian and English. Sent from `Leitagu <noreply@leitagu.ee>` unless `EMAIL_FROM` says otherwise; the sending domain must be verified in Resend.
- **Apps.** Shop and admin.
- **Settings.** `RESEND_API_KEY`; optional `EMAIL_FROM`, `EMAIL_REPLY_TO`; `APP_URL` (or `WEB_URL`) for the links.
- **Code.** `packages/core/src/server/mail.ts` (`sendMail`), templates in `packages/core/src/server/mailTemplates.ts`.
- **When it fails.** Sending never throws: a rejection, a timeout (10 seconds) or a network error is logged as `[mail] …` and reported as not sent, so signup and invites carry on. Without a key, links are printed to the console in development and nothing is sent in production, so new shoppers can't confirm their address; the admin app still shows each staff invite link to pass on by hand. Leitagu doesn't receive any webhooks from Resend.

### Migadu

- **What.** Migadu, the mail host of the team's shared partner mailbox (`leitagu@partner.ee`). The CRM uses it over standard IMAP and SMTP (`imapflow`, `mailparser`, `nodemailer`), not through an API, so any IMAP/SMTP host works; Migadu's servers are only the defaults.
- **Used for.** Copying every email in the inbox and the Sent folder into the CRM, and sending emails from a company's page. See [Email](crm.md#email) in the CRM page.
- **App.** CRM.
- **Settings.** `CRM_MAIL_ADDRESS` and `CRM_MAIL_PASSWORD` (both needed to connect); optional `CRM_MAIL_NAME` (default `Leitagu`), `CRM_IMAP_HOST` and `CRM_IMAP_PORT` (default `imap.migadu.com`, 993), `CRM_SMTP_HOST` and `CRM_SMTP_PORT` (default `smtp.migadu.com`, 465); `CRON_SECRET` for the scheduled read.
- **Endpoint.** `POST /api/cron/mail` on the CRM (GET also works), with `Authorization: Bearer` and `CRON_SECRET`, every five minutes (see [Scheduled jobs](scheduled-jobs.md#crm-mailbox-read)). It answers 401 without the right secret and 409 when the mailbox isn't connected.
- **Code.** `packages/core/src/server/crmMail.ts`; `apps/crm/src/app/api/cron/mail/route.ts`.
- **When it fails.** A folder that can't be read records its error in `crm_mail_state`, which the CRM shows next to "Last checked"; the other folder is still read. A failed send answers `send_failed` and nothing is saved. Without the settings, the CRM says the mailbox isn't connected.

### Shopify

- **What.** The Shopify Admin GraphQL API of each store that connects its Shopify shop, at `https://<shop>.myshopify.com/admin/api/<version>/graphql.json`.
- **Used for.** Importing the store's products, on a schedule or when asked.
- **Apps.** Partner app (data connections), and the admin app's MCP tools for staff who set up stores.
- **Settings.** `INTEGRATION_SECRET_KEY` encrypts each store's access token (required in production; if it changes, stores must reconnect). Optional `SHOPIFY_API_VERSION` (default `2026-07`).
- **Code.** `packages/core/src/server/integrations/fetchers.ts` (`fetchShopify`, `testShopify`), `sources.ts` (mapping), `secrets.ts` (encryption), `sync.ts` (the run).
- **Details for stores.** [Shopify](../partners/integrations.md#shopify) in the partner integrations guide.

### WooCommerce

- **What.** The public WooCommerce Store API of each connected shop (`/wp-json/wc/store/v1/products`). No key is needed.
- **Used for.** Importing the store's products. Leitagu reads the shop's `robots.txt` first and follows it, including its crawl delay.
- **Apps.** Partner app, and the admin app's MCP tools.
- **Code.** `packages/core/src/server/integrations/fetchers.ts` (`fetchWooCommerce`, `wooRobots`), `robots.ts`, `sources.ts`.
- **Details for stores.** [WooCommerce](../partners/integrations.md#woocommerce).

### Product feeds and CSV

- **What.** A feed file at an address the store gives (a Google Merchant Center XML feed, or CSV/TSV), or a CSV file uploaded in the partner app.
- **Used for.** Importing the store's products.
- **Apps.** Partner app, and the admin app's MCP tools.
- **Code.** `packages/core/src/server/integrations/sources.ts` (`parseFeed`, `csvToItems`), fetched with `safeFetchText()` in `fetchers.ts`.
- **Details for stores.** [Product feed](../partners/integrations.md#product-feed) and [CSV upload](../partners/integrations.md#csv-upload).

**For all store connections.**

- **Endpoint.** `POST /api/cron/sync` on the partner app (GET also works), with `Authorization: Bearer` and `CRON_SECRET`, every hour; it runs every Shopify, WooCommerce and feed connection that is due. The product pipeline backfill is `POST /api/cron/pipeline` with the same secret. How each runs in production is in [Scheduled jobs](scheduled-jobs.md).
- **Fetch safety.** Addresses that stores give us are fetched only through `safeFetch()` / `safeFetchText()`: public addresses only (checked when connecting), checked redirects, size and time limits. See [Feed fetch safety](../partners/integrations.md#feed-fetch-safety).
- **When a sync fails.** The run is recorded in `sync_runs` as `failed` or `partial` with its error and issues, which the partner and admin apps show. One failing connection doesn't stop the others. Products are never hidden automatically when data gets old; staff decide (see [Out-of-date data](../partners/integrations.md#out-of-date-data)).

### Store websites (sign-up proof)

- **What.** The home page of a store that signs up in the partner app.
- **Used for.** One of the ways to prove a store sign-up: Leitagu fetches the home page and looks for a meta tag with the claim's code. The other ways are an email address on the store's domain or staff approval.
- **App.** Partner (`/join`); claims are approved in admin.
- **Code.** `packages/core/src/server/storeClaims.ts`, `packages/core/src/shared/storeClaims.ts`. See [How a store joins](../partners/why-join.md#how-a-store-joins).

### AI assistant clients (MCP)

- **What.** The admin app is an MCP server and an OAuth 2.1 authorization server for AI assistants such as Claude (claude.ai, Desktop, mobile and Claude Code). This is an inbound integration: the assistants call Leitagu.
- **Used for.** Staff working in the CRM and setting up stores through an assistant, as themselves.
- **App.** Admin.
- **Settings.** `ADMIN_URL` must be exactly the public address, because it is the OAuth issuer.
- **Endpoints.** `/api/mcp`, `/.well-known/oauth-*`, `/oauth/authorize`, `/api/oauth/{register,token,revoke,authorize}`. The admin app fetches a client's metadata document (for Claude, from `claude.ai`) with a 5-second limit; codes are only sent to the person's own computer or to claude.ai.
- **Code.** `packages/core/src/server/oauth.ts`, `mcp.ts`, `mcpTools.ts`. Details in [AI assistants (MCP)](mcp.md).

### Estonian business register

- **What.** e-Äriregister (`ariregister.rik.ee`).
- **Used for.** Only a link: a CRM company with a registry code links to its register entry. Nothing is fetched from it.
- **Code.** `apps/crm/src/app/(crm)/companies/[id]/page.tsx`.

### What Leitagu does not use

- **Analytics.** Page views are counted by Leitagu itself: the shop and the hub send them to the shop's `/api/collect`, with no cookies and no outside service. See [Traffic analytics](traffic-analytics.md).
- **Login providers.** Accounts are Leitagu's own email and password; staff sign in to the CRM and partner app through the admin app.
- **Payments, maps, SMS and file storage.** None in the code. Product images are links to the stores' own image addresses, and room photos are sent to Claude, not stored.
- **Fonts and 3D.** Geist and three.js are npm packages bundled with the apps, not loaded from another site.

## Infrastructure

### Coolify on DigitalOcean

- **What.** Coolify, a self-hosted platform for Docker apps, on a DigitalOcean droplet. Its interface and API are at servers.leitagu.ee.
- **Used for.** Running the apps from their images and the PostgreSQL 16 database (resource `leitagu-postgres`), with the apps' environment variables and domains, and running the database backups.
- **Endpoints.** The `images` workflow calls `https://servers.leitagu.ee/api/v1/deploy?uuid=<app>&force=true` for each app and polls `/api/v1/deployments/<id>` until it finishes. The app uuids are in the workflow (they are not secret).
- **Settings.** `COOLIFY_DEPLOY_TOKEN`, a GitHub repository secret: a Coolify API token with the `deploy` and `read` abilities. Coolify itself needs registry credentials with `read:packages` to pull the private images (Keys & Tokens → registry credentials).
- **When it fails.** The deploy job marks an app failed if the request fails or the deployment doesn't finish within 10 minutes, carries on with the next app, and fails the run at the end. The apps that weren't deployed keep running their previous image.
- **Scheduled tasks and backups.** Coolify also runs the store sync and the CRM mailbox read as Scheduled Tasks, and the database backups; see [Scheduled jobs](scheduled-jobs.md) and [Backups and restore](backups.md).

### GitHub Actions and ghcr.io

- **What.** GitHub Actions builds the images; GitHub Container Registry stores them as `ghcr.io/leitagu/leitagu-<app>` (private).
- **Used for.** On a push to `main`: build the apps the change touches, start each image against a throwaway Postgres and check it, push it with the tags `latest` and `sha-<short commit>`, then deploy through Coolify in the order web, admin, crm, partner, docs. Pull requests build and check their images and push them as `pr-<number>`. **Actions → images → Run workflow** builds chosen apps by hand.
- **Code.** `.github/workflows/images.yml`, `Dockerfile`. Pushing uses the built-in `GITHUB_TOKEN`.
- **Details.** `DEPLOYMENT.md`, "Option C: container images".

### Cloudflare R2

- **What.** Cloudflare R2 object storage, bucket `leitagu-backups`, in the EU jurisdiction.
- **Used for.** Off-site copies of the database dumps and of Coolify's own backup, kept for 90 days. Coolify uploads them; the apps don't use R2.
- **Settings.** The R2 endpoint and access keys are set in Coolify and kept in the password manager, not in the repository.
- **Details.** [Backups and restore](backups.md).

### zone.ee (domain and DNS)

- **What.** zone.ee, the Estonian registrar where `leitagu.ee` is registered. Its DNS is managed there too, as for the team's other domains.
- **Names.** `leitagu.ee`, `www`, `admin`, `partner`, `crm` and `docs` point at the hosting (`DEPLOYMENT.md`, "What you need"), and `servers` at Coolify.
- **Who changes it.** Mihkel, in the zone.ee control panel. A new subdomain or a move to another server starts there.
- **Not Cloudflare.** Cloudflare is used only for R2 backups; `DEPLOYMENT.md` mentions Cloudflare Access only as an option in front of the staff apps.

## Related pages

- [Platform architecture](architecture.md): the apps, the database and all settings
- [Partner integrations guide](../partners/integrations.md): store connections in detail
- [Backups and restore](backups.md)
- [Scheduled jobs](scheduled-jobs.md)
- [AI assistants (MCP)](mcp.md)
- [Internal CRM](crm.md)
