How to Automatically Deploy Your Frontend upon Railway Updates

How to Automatically Deploy Your Frontend upon Railway Updates

· 6 min read

Recently, I was developing a Payload CMS 🔗 project for a client. The project follows a traditional frontend-backend architecture:

Railway automatically triggers deployments when code is pushed, which is convenient and straightforward. However, what if we want to deploy the frontend automatically after Railway deployments complete? Since backend changes typically involve API modifications, we want to automatically apply these changes to the frontend as well.

In this post, we’ll use Cloudflare Workers and GitHub Actions to achieve this automation.

Setting Up a Railway Webhook

Following the Railway documentation 🔗, we can easily set up a webhook in the dashboard:

  1. Open your existing project on Railway
  2. Click the Settings button in the top right corner
  3. Navigate to the Webhooks tab
  4. Enter your desired webhook URL
  5. Optional: specify which events to receive notifications for
  6. Click Save Webhook
Railway Webhook

As shown in the image, we need an endpoint to receive webhook events. We’ll use Cloudflare Workers to handle this. Let’s move on to the next step.

Creating Cloudflare Workers Functions

Cloudflare Workers provides one of the easiest ways to deploy serverless code. Let’s walk through the setup process.

Creating a Cloudflare Workers Project

  1. Go to your Cloudflare dashboard 🔗
  2. Click Workers & Pages
  3. Click the Create button
Cloudflare Dashboard

Start with the basic example by clicking Get started under Start with Hello World. Deploy the script, connect your GitHub repository, then clone the code locally to write custom functions.

Cloudflare Workers

Writing Custom Functions

After cloning the code locally, let’s implement our custom logic.

According to Railway’s documentation, we’ll receive a payload like this when webhook events occur:

{
"type": "DEPLOY",
"timestamp": "2025-02-01T00:00:00.000Z",
"project": {
"id": "[project ID]",
"name": "[project name]",
"description": "...",
"createdAt": "2025-02-01T00:00:00.000Z"
},
"environment": {
"id": "[environment ID]",
"name": "[environment name]"
},
"deployment": {
"id": "[deploy ID]",
"creator": {
"id": "[user id]",
"name": "...",
"avatar": "..."
},
"meta": {}
}
}

We want to deploy the frontend once the backend Railway project is successfully deployed. We only need the Railway project ID for validation.

Let’s define the TypeScript interface for the payload:

index.ts
interface RailwayPayload {
type: string;
timestamp: string;
project: {
id: string;
};
}

Next, we need to validate that the payload comes from the correct Railway project:

index.ts
function validateRailwayWebhook(
env: Env,
payload: unknown,
): payload is RailwayPayload {
if (payload && typeof payload === "object" && "project" in payload) {
const { project } = payload;
if (project && typeof project === "object" && "id" in project) {
return project.id === env.RAILWAY_PROJECT_ID;
}
}
return false;
}
Note

The Env type comes from Wrangler’s type generation and depends on your wrangler.toml or wrangler.jsonc configuration. We pass the RAILWAY_PROJECT_ID to the vars section in the config file.

Now we’ll trigger the remote GitHub Actions workflow that deploys the frontend. We’ll use the GitHub REST API 🔗 to create a custom repository dispatch event:

index.ts
async function dispatchGitHubAction({
env,
eventType,
clientPayload,
}: {
env: Env;
eventType: string;
clientPayload: unknown;
}) {
const githubToken = await env.GITHUB_TOKEN.get();
if (!githubToken) {
console.log("Missing GitHub token");
throw new Error("Missing GitHub token");
}
const githubRepo = env.GITHUB_REPO;
if (!githubRepo) {
console.log("Missing GitHub repository");
throw new Error("Missing GitHub repository");
}
const response = await fetch(
`https://api.github.com/repos/${env.GITHUB_REPO}/dispatches`,
{
method: "POST",
headers: {
Authorization: `Bearer ${githubToken}`,
Accept: "application/vnd.github+json",
"Content-Type": "application/json",
"X-GitHub-Api-Version": "2022-11-28",
"User-Agent": "cloudflare-workers",
},
body: JSON.stringify({
event_type: eventType,
client_payload: clientPayload,
}),
},
);
if (!response.ok) {
throw new Error(
`GitHub API error: ${response.status} ${response.statusText}`,
);
}
return response;
}
async function handleRailwayWebhook(request: Request, env: Env) {
try {
const payload = await request.json();
if (!validateRailwayWebhook(env, payload)) {
return new Response("Unauthorized", { status: 401 });
}
await dispatchGitHubAction({
env,
eventType: "railway_deploy",
clientPayload: {
projectId: payload.project.id,
},
});
return Response.json({
success: true,
message: "Railway deployment rebuild triggered",
});
} catch (_error) {
return new Response("Internal Server Error", { status: 500 });
}
}

The code is straightforward, but there are a few important details to note. We’re using Cloudflare’s secrets store 🔗 to securely store the GitHub Personal Access Token 🔗, which must have the appropriate repository permissions:

wrangler.jsonc
"secrets_store_secrets": [
{
"binding": "GITHUB_TOKEN",
"store_id": "your-store-id",
"secret_name": "GITHUB_TOKEN"
}
],

We must follow GitHub API’s header requirements precisely, otherwise the request will fail. The eventType is a custom event name that must match the one in your GitHub Actions workflow:

deploy.yaml
name: Deploy Web
on:
# Manual trigger for deployment as an escape hatch
workflow_dispatch:
# Deploy frontend automatically when backend Railway deployment completes
repository_dispatch:
types: [railway_deploy] # This must match the event type in your Workers code
jobs:
format:
uses: ./.github/workflows/format.yaml
build:
needs: format
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
name: Install pnpm
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: 24
cache: "pnpm"
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build OpenNext for Cloudflare
run: npx opennextjs-cloudflare build
working-directory: apps/web
env:
NEXT_PUBLIC_WEBSITE_URL: ${{ vars.NEXT_PUBLIC_WEBSITE_URL }}
NEXT_PUBLIC_PAYLOAD_SITE_URL: ${{ vars.NEXT_PUBLIC_PAYLOAD_SITE_URL }}
REVALIDATE_SECRET: ${{ secrets.REVALIDATE_SECRET }}
- name: Deploy
run: npx opennextjs-cloudflare deploy
working-directory: apps/web
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}

Finally, let’s expose our functions through the main handler:

index.ts
export default {
async fetch(
request: Request,
env: Env,
_ctx: ExecutionContext,
): Promise<Response> {
if (request.method !== "POST") {
return new Response("Method not allowed", { status: 405 });
}
const url = new URL(request.url);
switch (url.pathname) {
case "/railway-webhook":
return await handleRailwayWebhook(request, env);
// You can add other routes for different webhook handlers
default:
return new Response("Not found", { status: 404 });
}
},
} satisfies ExportedHandler<Env>;

Deploying Your Code

Now we’re ready to deploy. You can either:

  • Commit your code to trigger automatic deployment (if you connected GitHub in the dashboard)
  • Run pnpm run deploy to deploy manually

Optionally, you can assign a custom domain to your Workers project in the dashboard settings for a cleaner webhook URL.

Finally, return to the Railway dashboard and:

  1. Enter your endpoint URL (your-cloudflare-workers-url/railway-webhook)
  2. Select the Success event type
  3. Save the webhook configuration

That’s it! Now when your Railway project successfully redeploys, Cloudflare Workers will automatically trigger your frontend redeployment, keeping everything synchronized and up-to-date.

What’s Next?

Consider implementing a centralized workflow management system using Cloudflare Workers KV 🔗 to schedule and debounce operations across multiple projects. This could help you manage complex deployment pipelines more efficiently.


# Railway # Cloudflare Workers # GitHub Actions