RIDM Apps deployment guide

RIDM Apps is hosting that builds and deploys a connected GitHub repository and publishes it at <name>.ridm.ai. Sign up for it under My Apps in the console, and check prices on the pricing page. Terms of use are set by Chapter 3 of the Terms of Service (Korean). For a quick look at what's included, see the app hosting overview.

Deployment flow

  1. Sign up — In My Apps, pick a plan and an address (subdomain). The address can't be changed later.
  2. Connect GitHub — On the app screen, install the GitHub App ridm-apps-builder and choose a repository and branch. It only gets read access to the repository.
  3. Push — Every push to the connected branch starts a build. Build history and failure logs show on the app screen.
  4. First deploy — When the build succeeds, it passes automatic checks and goes live right away (below). Later pushes are deployed automatically too.

Dockerfile requirements

Next.js starter

Add output: 'standalone' to next.config.js and put the Dockerfile below at the repository root. If you don't have a public folder, delete that line.

FROM node:22-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci

FROM node:22-alpine AS build
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build

FROM node:22-alpine
WORKDIR /app
ENV NODE_ENV=production
COPY --from=build --chown=1000:1000 /app/public ./public
COPY --from=build --chown=1000:1000 /app/.next/standalone ./
COPY --from=build --chown=1000:1000 /app/.next/static ./.next/static
USER 1000
EXPOSE 3000
CMD ["node", "server.js"]

It doesn't have to be Next.js. Any stack works as long as the container answers on port 3000.

Runtime

Cookies

Many partner apps live under ridm.ai, so the gateway ties cookies to each app's own address. Your app code can use cookies as usual. But:

Database

Each app gets its own PostgreSQL database, and the connection details arrive as environment variables: DATABASE_URL (SSL required) plus PGHOST, PGPORT, PGDATABASE, PGUSER, PGPASSWORD and PGSSLMODE. The storage limit depends on the plan, and usage is shown on the app screen.

Database connections use TLS with full certificate verification. The certificate authority (Amazon RDS) is preinstalled in the container (NODE_EXTRA_CA_CERTS, PGSSLROOTCERT), so you don't need any SSL options in your code — new Pool({ connectionString: process.env.DATABASE_URL, max: 2 }) is enough. Each app gets up to 3 connections, so keep your pool size at 2 or less.

Environment variables

Add secrets in the console (people)

A person can add any name in the console. Names that AI tools can't set (below) are added this way too.

  1. Sign in at console.hrum.ai and open My Apps in the left menu.
  2. Scroll down the app card to the Environment variables section. Names already saved show •••••••• instead of a value.
  3. Type the name in the first field, Name (e.g. STRIPE_SECRET_KEY). Lowercase letters and other characters turn into uppercase and underscores as you type.
  4. Paste the value into the second field, Value. The field is masked like a password field.
  5. Click Save and you'll see a confirmation that the variable was saved and the app will restart shortly. Saving again under the same name changes the value.
  6. To delete one, click Delete on that row, then Delete in the confirmation dialog. Deleting restarts the app, and the value can't be recovered.

While moving a value:

Add secrets with AI (AI operations token)

For an AI coding tool to set environment variables for you, it needs an AI operations token. That's a console management token (hrum_cm_…, see Manage the console with AI) for which a person checked the hosting_ops permission when it was issued. With this permission, the tool doesn't ask for consent again for each task. Setting environment variables isn't part of the HRUM MCP, so values never pass through the AI model; use the HTTP paths below.

Names you can set through the AI path are only those ending in _KEY, _SECRET, _TOKEN, _PASSWORD, _PASSWD, _PASS, _SALT or _CLIENT_ID (e.g. STRIPE_SECRET_KEY, MASTER_KEY). Even among those, names that change the runtime or a connection address are refused: names ending in _URL, _URI, _ENDPOINT, _HOST or _PROXY; names starting with NODE_, LD_, DYLD_, PYTHON, JAVA_, _JAVA_, JDK_, RUBY, PERL, GIT_, NPM_CONFIG_, GLOBAL_AGENT_ or AWS_ENDPOINT_URL; and names such as HTTP_PROXY, HTTPS_PROXY, NODE_TLS_REJECT_UNAUTHORIZED, SSL_CERT_FILE, PATH and OPENAI_API_BASE. Other names (e.g. ADMIN_PATH, SMTP_HOST) are added by a person in the console. Deleting works through the AI path too (except reserved names).

Getting a token — valid for 30 days.

  1. Without authentication, the AI sends {"action":"token_issue","payload":{"scopes":["hosting_ops"],"label":"My app ops"}} to POST https://api.hrum.ai/api/v1/partners/v1/console-ai/consents. The response data contains consentUrl, userCode (8 characters), code and pollSecret.
  2. The AI shows the user only the consentUrl and the 8-character code. It doesn't show pollSecret.
  3. Within 10 minutes, the person opens the link in a browser signed in to the console, enters the 8-character code from the AI tool's screen in the Confirmation code field, checks hosting_ops and clicks Agree. Read the warning under the checkbox first (if the token leaks, someone could overwrite environment variables, deploy and restart).
  4. When the AI calls GET …/console-ai/consents/{code} with the X-Consent-Poll-Secret header, the first response after approval carries the token in data.token.value, exactly once. Don't print it; put it straight into a secret store (Keychain, a file with permissions 600, etc.).
  5. When you no longer need it, click Revoke under "AI console management tokens" on the console's API keys screen. It's blocked immediately. Each account can have up to 10 active tokens.

Calls — base URL https://api.hrum.ai, header Authorization: Bearer hrum_cm_…. The app id is data.apps[].id (a UUID) in the response of GET /api/v1/partners/v1/console-ai/hosting.

TaskRequestResponse
List namesGET /api/v1/partners/v1/console-ai/hosting/apps/{appId}/env{"success":true,"data":{"env":{"names":[…]}}} — no values
Add or changePUT …/hosting/apps/{appId}/env/{NAME} with body {"value":"…"}The updated list of names. The app restarts
DeleteDELETE …/hosting/apps/{appId}/env/{NAME}The updated list of names. The app restarts

Example (bash or zsh). The value and the token are never passed as command-line arguments; the value goes through standard input. Replace $APP_ID and the Keychain item names with your own.

# 1) Read secrets into shell variables (nothing is printed)
HRUM_CM_TOKEN=$(security find-generic-password -s hrum-console-ops -w)
VALUE=$(security find-generic-password -s myapp-STRIPE_SECRET_KEY -w)

# 2) Value goes through stdin, token through a header file: neither shows up in ps or shell history
printf '%s' "$VALUE" | jq -Rs '{value: .}' | curl -s -X PUT \
  -H @<(printf 'Authorization: Bearer %s' "$HRUM_CM_TOKEN") \
  -H 'Content-Type: application/json' --data-binary @- \
  "https://api.hrum.ai/api/v1/partners/v1/console-ai/hosting/apps/$APP_ID/env/STRIPE_SECRET_KEY" \
  | jq '.data.env.names // .'

# 3) Forget the value
unset VALUE

Once it's saved, the response shows only the list of names. Open the app's address to check that it restarted with the new value.

Deploy and read logs with AI (AI operations token)

With the same AI operations token (hosting_ops), you can start a deploy and check deploy status and the logs your app writes — without opening the site.

From HRUM MCP 0.3.3, MCP tools do the same: hrum_hosting_deploy (start a deploy), hrum_hosting_deploys (deploy status) and hrum_hosting_logs (runtime logs). Get a token with the hosting_ops permission through hrum_ai_connect first, and find the app id with hrum_hosting_status. Tools that don't use MCP use the HTTP paths below.

TaskRequestResponse
Start a deployPOST …/hosting/apps/{appId}/deploy with body {} or {"commit":"40-char sha"}{"deployId","commitSha","retried","status":"queued"}. Just like "Deploy now" in the console, it builds the latest commit on the connected branch (same first-deploy review and plan rules). commit is accepted only when it is that latest commit — older commits aren't supported yet (400 COMMIT_NOT_BRANCH_HEAD). Up to 2 per minute
Deploy statusGET …/hosting/apps/{appId}/deploys?limit=20Newest first: id, commitSha, status, queuedAt, startedAt, finishedAt, liveAt, failReason, scanFindings (only the file, line and rule flagged by the secret scan), logTail (end of the build log), startFailReason, startFailLog
Request logsPOST …/hosting/apps/{appId}/logs with body {"lines":100} (1–200)202 {"requestId"}
Fetch logsGET …/hosting/apps/{appId}/logs/{requestId}If status is pending, try again in 2–3 seconds; if delivered, read lines. After 3 minutes it becomes expired

Logs and failure reasons are masked before they're returned: environment variable values stored for that app, and key-shaped strings such as sk-…, AKIA…, Bearer …, JWTs and …_live_…, become [redacted]. Still, the first step is making sure your app doesn't log secrets. Every call (including refused ones) is recorded with the token, the app and the result.

The same paths exist for a console sign-in session or an HRUM MCP sign-in session: POST …/console/hosting/apps/{appId}/deploy-now (for MCP, only sessions created by signing up), GET …/console/hosting/apps/{appId}/deploys, POST …/console/hosting/apps/{appId}/logs with body {"lines":100} (1–200; for MCP, only sessions created by signing up) → GET …/logs/{requestId}. They only work for your own apps, and log lines are masked by the same rules. Fetched log contents are deleted 24 hours after they're delivered (or requested) (7 days from 2026-12-01; Terms, Article 20, Paragraph 5).

HRUM_CM_TOKEN=$(security find-generic-password -s hrum-console-ops -w)
API=https://api.hrum.ai/api/v1/partners/v1/console-ai/hosting/apps/$APP_ID
git push origin main
curl -s -X POST -H @<(printf 'Authorization: Bearer %s' "$HRUM_CM_TOKEN") \
  -H 'Content-Type: application/json' -d '{}' "$API/deploy" | jq '.data // .'
curl -s -H @<(printf 'Authorization: Bearer %s' "$HRUM_CM_TOKEN") \
  "$API/deploys?limit=3" | jq '.data.deploys[] | {id, commitSha, status, liveAt, failReason}'

Secret safety rules (for AI tools)

A real case: an AI tool building a sales management app tried to put the app's master key into an environment variable, but back then there was no operations path for AI, so it stopped there. Today, names ending in _KEY like MASTER_KEY can be set with an AI operations token, and other names are added by a person in the console. Either way, values never travel through chat.

Environment variable errors

Error bodies look like {"success":false,"statusCode":…,"message":"…"}. Environment variable errors have no code, so tell them apart by the status code and the start of the message below. The API returns these messages in Korean, so the start of each message is shown as returned, followed by its meaning. The full messages are in llms.txt.

StatusStart of messageCause → fix
400이름은 대문자·숫자·밑줄만 (names: uppercase, digits, underscores only)Name breaks the rules → uppercase, digits and underscores only, not starting with a digit, 64 characters max
400플랫폼이 쓰는 이름이라 (name used by the platform)PORT, HOSTNAME, NODE_OPTIONS, DATABASE_URL, or a RIDM_ / PG prefix → use another name
400AI 연결이 자동으로 넣는 이름이라 (set automatically by AI Connect)HRUM_API_KEY → this app has AI Connect turned on, so the platform sets it. To set it yourself, turn AI Connect off
400이 계정에서 발급한 API 키만 넣을 수 있어요 (only keys issued by this account)Use a key from API keys in your own console
400AI 연결이 앱마다 만든 키는 넣을 수 없어요 (per-app AI Connect keys not allowed)Use a key issued under API keys in the console
400값을 입력해 주세요 (enter a value)The value is empty
400값에 줄바꿈을 넣을 수 없 (no line breaks in values)Multi-line value (a PEM private key, etc.) → convert it to a single base64 line and decode it in your app
400값은 2048바이트까지 (values up to 2048 bytes)The value is over 2KB → keep large values in the database or an external secret store
400실행 환경을 바꾸는 이름이라 AI 경로 (changes the runtime; not via the AI path)A name the AI path refuses → a person adds it in the console
400AI 경로(AI 운영 토큰·MCP)로는 _KEY (AI path only accepts _KEY…)Not a secret-name suffix → rename it, or have a person add it in the console
400Validation failed (uuid is expected)The app id isn't a UUID → use apps[].id from GET /console-ai/hosting
401MGMT_TOKEN_INVALIDToken missing, expired or revoked → issue a new one
403MGMT_SCOPE_DENIED (scope: "hosting_ops")The token doesn't have hosting_ops → issue a new one with it checked
404앱을 찾을 수 없 (app not found)Another account's app or a deleted app → check the app id
404없는 환경변수 (no such environment variable)The name you're deleting doesn't exist → check the list of names
409해지된 앱은 환경변수를 바꿀 수 없 (cancelled app)The app is being cancelled → it can't be changed
409환경변수는 앱당 50개까지 (up to 50 per app)Delete names you don't use and try again
429—Adding and deleting are limited to 30 per minute → try again shortly
503환경변수 저장소가 준비되지 않 (variable store not ready)A server-side problem → try again shortly; contact us if it continues

AI Connect

AI Connect lets your app call the Saju and AI APIs: the platform adds an API key that only this app uses. It's available on every account.

  1. Turn it on — Open the app's details under Apps in the console and turn on "Use AI Connect". An app-only key is created and added as the environment variable HRUM_API_KEY, and the app restarts.
  2. Call — In your app's server code, read process.env.HRUM_API_KEY and put it in the X-Partner-Api-Key header. The key value isn't shown even in the console. Don't put it in code that's sent to the browser.

While it's on, you can't set HRUM_API_KEY yourself. If you set a value yourself, delete it before turning AI Connect on (otherwise you'll get an error asking you to delete the existing HRUM_API_KEY environment variable first). Charges from this key come out of your credits, and once it reaches the daily usage limit set in the app's details, it stops for that day. Turning it off cuts the key immediately; turning it back on creates a new key. This key can't call the payment APIs — accepting payments in your app uses a key issued in the console.

Accept payments in your app

You can take payments from your app's users through the HRUM Payments checkout page. Selling opens once you register as a seller (individual, sole proprietor or corporation) and are approved. Register as a seller under Payouts in the console. The conditions are set by Articles 32–39 of the Terms of Service (Korean). Apps on the free plan can sell too. However, while an app is not being served because it hit its monthly transfer limit or was suspended, opening new checkouts is refused with 409 APP_NOT_SERVING. Checkouts already opened, order lookups and refunds still work.

  1. Add an API key — Put a key issued under API keys in the console into an environment variable (e.g. HRUM_PARTNER_KEY). The HRUM_API_KEY that AI Connect adds can't call the payment APIs. Use the key only in server code and never send it to the browser.
  2. Register a product — POST /api/v1/partners/v1/storefront/skus with body {"appId","name","priceWon"}. Payments are only taken at the price registered here.
  3. Open a checkout — From your server, send POST /api/v1/partners/v1/storefront/checkouts with body {"appId","skuId","qty","returnUrl"}, then send the user to checkoutUrl from the response. returnUrl must be an https address on this app (<name>.ridm.ai or a connected domain).
  4. Check the result — After paying, the user comes back to returnUrl?orderId=…. On your server, call GET /api/v1/partners/v1/storefront/orders/{orderId} and confirm status is paid before delivering the product. Don't trust the value in the URL alone.

Every request needs the X-Partner-Api-Key header. For refunds, email the order number to [email protected]. Once refunded, the order status changes to canceled or partial_canceled.

const res = await fetch("https://api.hrum.ai/api/v1/partners/v1/storefront/checkouts", {
  method: "POST",
  headers: { "Content-Type": "application/json", "X-Partner-Api-Key": process.env.HRUM_PARTNER_KEY },
  body: JSON.stringify({ appId, skuId, qty: 1, returnUrl: "https://myapp.ridm.ai/paid" }),
});
const { checkoutUrl } = (await res.json()).data;

What each key permission can call

You can choose permissions when you create a key. Calling a path outside the chosen permissions returns 403 KEY_SCOPE_MISSING. Existing keys with no permissions chosen can still call every path. All paths are in the OpenAPI document.

Settlement data API

Get the same numbers as the Console → Payouts screen from your server. Your key only sees your own data. The period from / to uses Korean dates (YYYY-MM-DD, both ends included); leave it empty for the current month. Up to 366 days at a time.

const res = await fetch("https://api.hrum.ai/api/v1/partners/v1/settlement/summary?from=2026-11-01&to=2026-11-30", {
  headers: { "X-Partner-Api-Key": process.env.HRUM_PARTNER_KEY },
});
const { sales, payouts, payoutFee } = (await res.json()).data;

Seller webhooks

We notify your server when a payment completes, a refund happens or a payout is made. Only https:// addresses are accepted (ports 443 and 8443), and private or internal addresses can't be registered.

  1. Register an address — PUT /api/v1/partners/v1/webhooks/endpoint with body {"url":"https://myapp.ridm.ai/hrum/webhook"}. The secret (whsec_…) in the response is shown only this once. Put it in the server environment variable HRUM_WEBHOOK_SECRET right away. To get a new one, send {"url": …, "rotateSecret": true}.
  2. Events — order.paid (payment completed), order.refunded (one refund), payout.scheduled (payout scheduled), payout.completed (payout completed), webhook.test (POST /webhooks/test). The body is {"id","type","created","data"} and contains no buyer information. Each occurrence is created only once with the same id, so use id to filter out duplicates.
  3. Verify the signature — Header Hrum-Signature: t=<unix seconds>,v1=<hex>. v1 is HMAC-SHA256 with your secret over (t + "." + the raw body as received). Reject any t older than 5 minutes.
  4. Retries — If you don't answer with a 2xx within 10 seconds, we retry after 1 minute, 5 minutes, 30 minutes, 2 hours and 12 hours (up to 6 attempts). View delivery records with GET /webhooks/deliveries and redeliver with POST /webhooks/deliveries/{eventId}/redeliver.
const crypto = require("node:crypto");
function verifyHrumWebhook(rawBody, header, secret, toleranceSec = 300) {
  const parts = Object.fromEntries(String(header || "").split(",").map((p) => p.trim().split("=")));
  const t = Number(parts.t);
  if (!Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
  const want = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest();
  const got = Buffer.from(parts.v1 || "", "hex");
  return got.length === want.length && crypto.timingSafeEqual(got, want);
}
// Express: verify the raw body before parsing it as JSON
app.post("/hrum/webhook", express.raw({ type: "application/json" }), (req, res) => {
  const raw = req.body.toString("utf8");
  if (!verifyHrumWebhook(raw, req.get("Hrum-Signature"), process.env.HRUM_WEBHOOK_SECRET)) return res.sendStatus(400);
  const event = JSON.parse(raw); // if event.id was already handled, just return 200
  res.sendStatus(200);
});

First deploy and automatic checks

When the first build succeeds, it passes automatic checks, is approved automatically and the address goes live right away. Paid plan charges aren't deducted until the first deploy is live. If the repository contains secrets (keys, tokens, passwords), the build fails. Deploys still go out automatically when a new outbound destination or a new risky pattern is found, or when the repository owner changes; those changes are recorded. Every automatic approval and check result is recorded and reviewed by operators afterwards, and publication can be stopped if there's a problem. If publication is stopped, we tell you why by email and on the app screen. For prohibited activities, see Article 26 of the Terms (Korean).

Pricing and limits

Price, memory, database and monthly transfer for each plan are on the pricing page. Paid plan fees are deducted from your topped-up credits every month. If your balance is short on the billing date, the app stops, and it resumes automatically once you top up. Monthly transfer is the sum of responses sent to visitors and traffic your app sends out (including external API calls, uploads and database writes). For WebSocket, frames in both directions count. When you hit the limit, open WebSockets are also closed with 1008. At the limit, both publication and the app stop (resuming on the 1st of the next month); on paid plans, buying a transfer block on the app screen brings it back within a few minutes. We email you at 80% and when you hit the limit.

Backup and cancellation

Contact: contact@float.do