# Railway deployment notifications on your phone

> Get a push notification when a Railway deployment succeeds, fails or crashes, loudest when a production service crashes.

- Company: Railway (https://www.justpush.io/recipes/railway)
- Event: `Deployment.crashed` (Deployment status)
- Tags: Hosting & deployment, Deployments
- Install: https://studio.justpush.io/recipes/railway/deployment
- Web page: https://www.justpush.io/recipes/railway/deployment

## Setup

1. Click Install in Studio and sign in. The recipe is added to your account and you get a personal webhook URL.
2. In Railway, open your project and go to Settings > Webhooks.
3. Paste your webhook URL (shown in Studio after install) as the webhook URL and pick the Deployment status events.
4. Click Save Webhook. It covers every service and environment in the project.
5. In Railway, open the project's **Settings → Webhooks**, paste the URL above and click **Save Webhook**. If you pick events, choose the deployment status changes. The webhook covers every service and environment in the project.
6. The recipe pushes for deployments that **succeed**, **fail**, **crash** or **run out of memory**, and for deploys that **need approval**. Building, deploying, removed and sleeping states stay quiet. Add `?failures=1` to only get crashes and failures:
7. ```
8. your webhook URL (shown in Studio after install)?failures=1
9. ```
10. An environment called `production` gets the loudest pushes: Highest for a crash, High for a failed deploy.
11. Railway's **Test Webhook** button is sent from your browser and its payload isn't documented, so it may not produce a push. Trigger a redeploy to test instead.
12. Railway doesn't sign webhooks. Keep your endpoint URL private.

## Code

Studio calls `handleRequest(request)` with the incoming webhook (`{ method, headers, body, query, raw }`) and sends the message object it returns. Returning `null` sends nothing.

```js
// Railway → deployment status changed
// Fires when a Railway project webhook reports a deployment reaching a final state
// (succeeded, failed, crashed, out of memory) or waiting for approval.
// Docs: https://docs.railway.com/observability/webhooks

// Deployment states (lowercased, from the "Deployment.<state>" type or the legacy
// "status" field), their labels, and how loud to be in production vs elsewhere.
const STATES = {
    // A crashed service is down right now; in production that's the loudest push.
    crashed: { emoji: "💥", verb: "crashed", production: 2, other: 1 },
    oomkilled: { emoji: "💥", verb: "ran out of memory", production: 2, other: 1 },
    // A failed deploy needs you soon; the previous deployment normally keeps running.
    failed: { emoji: "❌", verb: "deploy failed", production: 1, other: 0 },
    // Someone has to approve the deploy before it goes out.
    needsapproval: { emoji: "⏸️", verb: "deploy needs approval", production: 0, other: 0 },
    // Good news is routine.
    success: { emoji: "✅", verb: "deployed", production: -1, other: -1 },
    deployed: { emoji: "✅", verb: "deployed", production: -1, other: -1 },
    redeployed: { emoji: "✅", verb: "redeployed", production: -1, other: -1 },
}

// States that count as problems, for ?failures=1.
const PROBLEMS = ["crashed", "oomkilled", "failed"]

// Look a key up in one of the tables above, ignoring inherited names like "toString".
function pick(table, key, fallback) {
    return Object.prototype.hasOwnProperty.call(table, key) ? table[key] : fallback
}

// Only link to real http(s) URLs.
function link(url, cta) {
    return typeof url === "string" && /^https?:\/\//.test(url) ? [{ cta, url }] : []
}

// A plain, non-empty string or null.
function text(value) {
    return typeof value === "string" && value.trim() ? value.trim() : null
}

function handleRequest(request) {
    const body = request.body && typeof request.body === "object" ? request.body : {}
    const type = text(body.type) ?? ""

    // Current format: type "Deployment.crashed". Older webhooks sent type "DEPLOY"
    // with the state in a top-level "status" (e.g. "CRASHED").
    let state = null
    if (/^deployment\./i.test(type)) state = type.slice("deployment.".length)
    else if (type.toUpperCase() === "DEPLOY") state = text(body.status)
    state = String(state ?? "").toLowerCase().replace(/[^a-z]/g, "")
    const event = pick(STATES, state, null)

    // Guard: only deployment states worth a push (building, deploying, removed … stay quiet).
    if (!event) {
        console.log(`Ignoring Railway "${type || "unknown"}" event`)
        return null
    }

    // ?failures=1 on the webhook URL keeps only crashes and failures.
    const failuresOnly = ["1", "true", "yes"].includes(String(request.query?.failures ?? "").toLowerCase())
    if (failuresOnly && !PROBLEMS.includes(state)) {
        console.log(`Skipping "${state}" (the URL has ?failures=1)`)
        return null
    }

    // The current format nests names under "resource"; the legacy one had them at the top.
    const resource = body.resource && typeof body.resource === "object" ? body.resource : body
    const details = body.details && typeof body.details === "object" ? body.details : body.deployment?.meta ?? {}
    const service = text(resource.service?.name) ?? text(resource.project?.name) ?? "Your service"
    const environment = text(resource.environment?.name)
    const production = /^prod(uction)?$/i.test(environment ?? "")

    const commit = (text(details.commitMessage) ?? "").split("\n")[0]
    const author = text(details.commitAuthor)
    const message = [text(resource.project?.name), text(details.branch), commit, author ? `@${author}` : null].filter(Boolean).join(" · ")

    // Link to the service in the dashboard when the IDs are there.
    const projectId = text(resource.project?.id)
    const serviceId = text(resource.service?.id)
    const environmentId = text(resource.environment?.id)
    const url = projectId && serviceId
        ? `https://railway.com/project/${encodeURIComponent(projectId)}/service/${encodeURIComponent(serviceId)}` +
          (environmentId ? `?environmentId=${encodeURIComponent(environmentId)}` : "")
        : null

    return {
        title: `${event.emoji} ${service} ${event.verb}${environment ? ` (${environment})` : ""}`,
        message: message || "Deployment update from Railway",
        topic: "Railway",
        priority: production ? event.production : event.other,
        buttons: link(url, "Open in Railway"),
    }
}
```

## Railway Deployment.crashed webhook payload (sample)

```json
{
  "type": "Deployment.crashed",
  "details": {
    "id": "8107edff-4b8e-44fc-b43a-04566e847a2a",
    "branch": "main",
    "source": "GitHub",
    "status": "CRASHED",
    "commitHash": "3f2a1bc9e7d84c5a9b0e6f1d2c3b4a5968778695",
    "commitAuthor": "janedoe",
    "commitMessage": "Add rate limiting to the API\n\nCloses #88"
  },
  "resource": {
    "project": {
      "id": "0f1e2d3c-4b5a-4968-8776-a5b4c3d2e1f0",
      "name": "acme-shop"
    },
    "service": {
      "id": "3c4d5e6f-7a8b-4c9d-8e0f-1a2b3c4d5e6f",
      "name": "api"
    },
    "workspace": {
      "id": "5c1d2e3f-4a5b-4c6d-8e7f-9a0b1c2d3e4f",
      "name": "Acme"
    },
    "deployment": {
      "id": "8107edff-4b8e-44fc-b43a-04566e847a2a"
    },
    "environment": {
      "id": "7a8b9c0d-1e2f-4a3b-8c4d-5e6f7a8b9c0d",
      "name": "production",
      "isEphemeral": false
    }
  },
  "severity": "ERROR",
  "timestamp": "2026-09-29T08:32:10.311Z"
}
```

## FAQ

### Does this work on iPhone and Android?

Yes. Install the JustPush app from the App Store or Google Play and sign in. Every phone signed in to your account gets the notification.

### Do I need to write code?

No. Install the recipe in Studio, paste your webhook URL into Railway and you are done. The code is there if you want to change the text, the sound or the buttons.

### Can I change what the notification says?

Yes. After install the recipe's code is yours. Edit it in Studio and test it against the sample payload before you save.

### What does it cost?

JustPush is free for 30 days. After that it's $19.99 a year, or $39.99 once. There is no extra charge for recipes.
