---
title: "Building a SaaS application on Railway"
description: "Zero to one, building a SaaS application on Railway"
date: 2024-12-13T00:00:00.000Z
updated: 2026-10-29T11:03:21.696Z
topics: ["Guides"]
tags: []
url: https://railway.com/guides/building-a-saas-application
---

# Building a SaaS application on Railway

*Software as a Service (SaaS) is a business where you provide value to your users using your software without them having to install or maintain it.  A SaaS doesn’t have to be a full-blown software suite but it can also be as simple as a Slack Bot or a ToDo app.*

## Why build a SaaS business

Building a SaaS business is one of the most lucrative avenues for software engineers because it provides:

- **Space to experiment with tech:** an opportunity to experiment with new technologies you don’t get to work with at your full-time job.
- **Lessons beyond tech:** gives real-life experience of product management that teaches you a lot more than the engineering aspect of it.
- **Extra income:** a successful SaaS business can generate a good side income or even a full-time income.

In this tutorial, you’ll build a SaaS application for a job application board and deploy it on the web to your custom domain.

## Plan the SaaS

Before starting development it’s important to understand the project requirements and expectations. It is also important to start with the most basic requirements for the Minimal Viable Product (MVP) to test your SaaS idea.

### Requirements

This job board application has the following requirements:

- Hiring managers from companies can add new jobs to the board after logging in.
- Job seekers can apply to these job posts by filling out a form without logging in.

In terms of features, the application will have the following pages:

- For job seekers:
    - A home page that displays all open job posts.
    - A job details page displays the job description and a form for applying for the position.
- For hiring managers:
    - Login/sign-up page.
    - A page to create the job post.
    - A page to view applicants who applied for a particular job post.

### Technical design

Now that the feature specifications are done. You can choose a tech stack that provides the best developer experience to build it. The following technologies match the requirements laid out previously:

- [Remix](https://remix.run/) for the full-stack development because it is built on React and supports server-side rendering.
- [PostgreSQL](https://www.postgresql.org/) for data storage because it is easy to get started and scale later on.
- [Clerk](https://clerk.com/) for authentication and user management.

## Build the application

<aside>
➡️

Follow along using [this GitHub repository](https://github.com/anshumanbhardwaj/job-board).

</aside>

To get started, create a Remix project by running the following command in the CLI:

```bash
npx create-remix@latest
```

This is an interactive command that will bootstrap a Remix project with [TailwindCSS](http://tailwindcss.com/) and [TypeScript](https://www.typescriptlang.org/). Choose the following options when prompted:

- Where should we create your new project?:  `job-board`.
- Initialise a new git repository?: Yes.
- Install dependencies with npm?: Yes.

### Set up shadcn/ui

You’ll use [shadcn/ui](https://ui.shadcn.com/docs/cli) as the UI library for consistent design and fast development. To get started, run the following command.

```bash
npx shadcn@latest init
```

This is an interactive command that will set up shadcn/ui with all its dependencies. Choose the following options when prompted:

- Which style would you like to use?**:** New York.
- Which color would you like to use as the base color?**:** Neutral.
- Would you like to use CSS variables for theming?**:**  Yes.

Now install the components you’ll be using to build the application by running this command in your project: 

```bash
npx shadcn@latest add button input textarea card label separator
```

### Add PostgreSQL database

You’ll use PostgreSQL to store the application data such as the job posts and applicants. The following diagram gives an overview of the data model for this application:

![Job Board Application ERD](https://cms.railway.com/media/eebe983490936a4054fa4435e6d584b1860e2793f5675bdb5502a100cca0ef74.png)

To integrate the Postgres database, you’ll use [Drizzle ORM](https://orm.drizzle.team/docs) with [postgres.js](https://github.com/porsager/postgres) driver that lets you use Postgres in the TypeScript application. Follow these steps to get started with Drizzle:

- Run `npm i drizzle-orm nanoid postgres` in the project command line to install the dependencies.
- Run `npm i -D drizzle-kit dotenv` to add utilities for development.
- Create a file `app/db.server/index.ts` to initialize the drizzle orm:

```tsx
import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";
import "dotenv/config";

const queryClient = postgres(String(process.env.DATABASE_URL));
export const db = drizzle(queryClient);
```

> `.server` a naming convention in Remix used to explicitly exclude files from the client-side JS bundle.
> 
- Create a file [`app/db.server/schema.ts`](https://github.com/anshumanbhardwaj/job-board/blob/0f2c0c24d8751eca91349ce727b148a27f07a15b/app/db.server/schema.ts) with the schema definitions and export the schema types.
- Create a file named [`drizzle.config.ts`](https://github.com/anshumanbhardwaj/job-board/blob/main/drizzle.config.ts) at the project root to configure drizzle credentials.
- Add database scripts to the [`package.json`](https://github.com/anshumanbhardwaj/job-board/blob/main/package.json) file:

```tsx
  "scripts": {
		// other scripts..
    "generate": "drizzle-kit generate",
    "migrate": "drizzle-kit push"
  },
```

These commands will come in handy for managing database schema migrations. To learn more about schema migrations, see [Drizzle migrations fundamentals](https://orm.drizzle.team/docs/migrations).

- Run `npm run generate` to generate the first migration file. This should create a new `drizzle` folder in the project. You should check this into your Git project.

> For local development you need to run Postgres on your computer, you can use Docker for it. To follow the next step, you must have docker installed on your computer.
> 
- Run the following command to start Postgres server on your computer:

```bash
docker run -e POSTGRES_USER=admin -e POSTGRES_PASSWORD=mysecretpassword -e POSTGRES_DB=pgdb -p 5432:5432 -d postgres
```

- Create a `.env` file in your project and add `DATABASE_URL` variable to it:

```bash
DATABASE_URL=postgres://admin:mysecretpassword@127.0.0.1:5432/pgdb
```

> Make sure that the **env** file is not checked into your Git project.
> 
- Run `npm run migrate` to apply the schema migrations to the local database.

You’re now all set to start working with the local Postgres database.

### Set up Clerk

To get started, sign up for a Clerk account if you don’t have one and then login into your [Clerk dashboard](https://dashboard.clerk.com/). Then follow these steps:

- Click **Create application.**
- Fill in the application name and select email login as the only sign-in option.
- Click **Create application.**
- Go to **Configure > API keys** and select **Remix** as the framework.
- Copy the credentials.
- Paste the credentials in the `.env` file.

### Add authentication

Inside your project follow these steps to add authentication using Clerk:

- Run `npm install @clerk/remix` in the terminal to install the dependencies.
- Update the [`app/root.tsx`](https://github.com/anshumanbhardwaj/job-board/blob/main/app/root.tsx) file to wrap the application in the `ClerkApp` higher order component. This ensures that the application has access to the authentication context and utilities from Clerk.
- Create a new file [`routes/sign-up.$.tsx`](https://github.com/anshumanbhardwaj/job-board/blob/main/app/routes/sign-up.%24.tsx) to render the sign up page using the `SignUp` component.
- Create a new file [`routes/sign-in.$.tsx`](https://github.com/anshumanbhardwaj/job-board/blob/main/app/routes/sign-in.%24.tsx) to render the sign in page using the `SignIn` component.
- Add the following content to the `.env` file for server-side auth routing:

```tsx
CLERK_SIGN_IN_URL=/sign-in
CLERK_SIGN_UP_URL=/sign-up
CLERK_SIGN_IN_FALLBACK_URL=/
CLERK_SIGN_UP_FALLBACK_URL=/
```

This will configure Clerk authentication provider and utilities throughout the application, which you’ll use in the next steps.

### Build pages for job seekers

- Update the [`app/routes/_index.tsx`](https://github.com/anshumanbhardwaj/job-board/blob/main/app/routes/_index.tsx) file with the code to show all the available jobs to the visitor. 

This code fetches the jobs from the database using the `loader` function and displays them to the user.

![Running locally - page for job seekers](https://cms.railway.com/media/1b0ecea627a0e11436ce7b3d39eea781046f59fee325da7096f2a5b0f4ebd445.png)

To render the job details markdown content as HTML, install the [showdown](https://github.com/showdownjs/showdown) package.

- Run the following command:

```bash
npm i showdown
npm i -D @types/showdown
```

Now add the typography plugin for Tailwind CSS to apply styles for markdown content. 

- Install the package by running `npm install -D @tailwindcss/typography`
- Add the plugin to the [`tailwind.config.ts`](https://github.com/anshumanbhardwaj/job-board/blob/main/tailwind.config.ts) file as follows:

```tsx
/** @type {import('tailwindcss').Config} */
module.exports = {
  theme: {
    // ...
  },
  plugins: [
    require('@tailwindcss/typography'),
    // ...
  ],
}
```

- Now create a file named [`app/routes/jobs.$jobId.tsx`](https://github.com/anshumanbhardwaj/job-board/blob/main/app/routes/jobs.%24jobId.tsx).

This file will create the route for job post details from where the visitor can apply for the job.

The `$` in the file name creates a [dynamic route](https://remix.run/docs/en/main/file-conventions/routes#dynamic-segments) and lets you access the `jobId` param at runtime.  

The `loader` function provides the job details data and creates the HTML markup for the markdown content. The `action` function handles the `POST` request from the job application `<Form>`  and then saves the resume (PDF file) to the server disk and the applicant details in the database.

![Running locally - Job posting page](https://cms.railway.com/media/c928e76d500aa20e34c3a746961ee3675313aeabc14d8fcaca2ef39da2d6b944.png)

### Build pages for hiring managers

The user journey for a hiring manager will be as follows:

![User journey for a hiring manager](https://cms.railway.com/media/9a4faf2ffbf4ed96ba481cd3d735f77478b250090684260a540a3d4eb53220ec.png)

- Create a file [`app/routes/dashboard.tsx`](https://github.com/anshumanbhardwaj/job-board/blob/main/app/routes/dashboard.tsx)  for the dashboard layout.
This file acts as the root of the dashboard and handles the authentication check on the server side data loader. It redirects the user to the `/sign-in` page if they’re not logged in. The `<Outlet />` component renders all the child or nested routes under the dashboard.
- Create a [`app/routes/dashboard._index.tsx`](https://github.com/anshumanbhardwaj/job-board/blob/main/app/routes/dashboard._index.tsx) file for the dashboard home page and display all the job posts created by the logged in hiring manager.
- Now create a [`app/routes/dashboard.new.tsx`](https://github.com/anshumanbhardwaj/job-board/blob/main/app/routes/dashboard.new.tsx) file for the `/dashboard/new` route.
This is the page from where the hiring managers will create new job posts.

The page will render as shown in the following image:

![Running locally - New job post page](https://cms.railway.com/media/fe8afde23a2d258cbc1f5aac9edb6bac4e2ad06ba7fd2c09a0a8461ab83fce58.png)

To let the hiring managers manage their job posts -

- Create another layout page [`app/routes/dashboard.$jobId.tsx`](https://github.com/anshumanbhardwaj/job-board/blob/main/app/routes/dashboard.%24jobId.tsx) with a nested layout to display the `/edit` and `/applicants` pages.

The edit job post page is similar to the new job post page but pre-filled with the default form values. 

- Create a new file [`app/routes/dashboard.$jobId.edit.tsx`](https://github.com/anshumanbhardwaj/job-board/blob/main/app/routes/dashboard.%24jobId.edit.tsx) with the code to fetch job data with the `jobId` and pre-fill edit job post form with default values.

![Running locally - Edit job post page](https://cms.railway.com/media/dea519de5e34c2aab41d052273e04d80ec0edf53f3858a65926b64ec3d0fc1b1.png)

To display the applicants for a job post -

- Create a new file [`app/routes/dashboard.$jobId.applicants.tsx`](https://github.com/anshumanbhardwaj/job-board/blob/main/app/routes/dashboard.%24jobId.applicants.tsx).

The `loader` function fetches the list of applicants for the current job post and lets the hiring manager download their resumes. The **Download resume** button is a download link to a route you’ll learn about in the next steps.

![Running locally - Applicants page](https://cms.railway.com/media/7e08f98cadc39896a6cf7e9e694837ef4f318b9b82d9af1938d7460779b4349b.png)

To let the users download the resumes, create a resource route in that only has a loader and returns a file response. 

- Create a new file [`app/routes/dashboard.$jobId.$applicantId.resume.ts](https://github.com/anshumanbhardwaj/job-board/blob/main/app/routes/dashboard.%24jobId.%24applicantId.resume.tsx)x`

In this file the `loader` function reads the resume file from the disk and returns it as the response.

## Deploy the application

The MVP application is now ready and it’s time to deploy it. To deploy this application you can choose any cloud provider but setting up a virtual private server (VPS) is rather complicated and time-consuming to start with. To get started quickly, you can use a platform like [Railway](https://railway.com/) that manages the low-level details of application deployment and exposes a seamless developer experience.

Before proceeding

- Add a new script to the [`package.json`](https://github.com/anshumanbhardwaj/job-board/blob/main/package.json) to run database migration in production before starting the server:

```tsx
  "scripts": {
	  // other scripts...
    "railway": "npm run migrate && npm run start"
  },
```

To deploy your application to Railway follow these steps:

- Push your code to a GitHub repository.
- [Create a new account](https://railway.com/?referralCode=40FqGU) on Railway and sign into the [dashboard](https://railway.com/dashboard).
- Click **New > Deploy from GitHub > Select the repository.**
- Click **Deploy now**.

This will create a new project with an application service and an automatic deployment will start. Railway will now automatically deploy the application whenever there’s a new commit on the `main` branch of your GitHub repository.

![Job board deploying in Railway](https://cms.railway.com/media/b66c2f57df0973bee7970fb5c62876705b255b8106452ceaa6b31555e7e36e7a.png)

- Go to **job-board > Settings > Deploy > Custom Start Command and** enter the `npm run railway` script you created earlier.
- Click **Save > Deploy**. This will redeploy the application.

![Adding a custom start command in Railway](https://cms.railway.com/media/1ffbc01ae52456eacc25ee35fbe83c759aef69eb9743457c7e34f3b2bb381673.png)

The deployment will crash because it’s missing the important credentials such as the `DATABASE_URL` and Clerk configuration. Close the **job-board** service pane.

### Add persistent storage

The application stores applicant resumes on the server disk but Railway services don’t have persistent volumes by default. To fix this, follow these steps:

- Open the project page in the Railway dashboard.
- Right-click on the **job-board** service card.
- Click **Attach volume.**
- In the mount path input field, enter `/resumes` . This is because `/resumes` is the directory that the file upload action is using.
- Click **Deploy.**

A persistent volume is now attached to your service.

### Add production database

To create a Postgres database in this project, follow these steps:

- Open the project page in your Railway dashboard.
- Click **Create.**
- Click **Database.**
- Click **Add PostgreSQL.**

This will add a PostgreSQL service to your project. 

![Adding Postgres to project in Railway](https://cms.railway.com/media/c1eb7d0b694fae308e06d137806d19567845313be389b4fbd833e5bcd838f278.png)

### Add environment variables

To add the environment variables, follow these steps:

- On your Railway dashboard, go to **job-board > Settings > Variables.**
- Click **Raw Editor:** Raw editor dialog opens.
- Copy the variables from your local `.env` file and paste in the raw editor dialog.
- Click **Update variables.**

> For brevity, this article is using the Clerk development credentials. You should change those before going to production. To learn more about deploying Clerk authentication to production, refer to [Clerk documentation](https://clerk.com/docs/deployments/overview)
> 
- For the `DATABASE_URL` variable, use the [Reference Variables](https://docs.railway.com/variables#reference-variables) from the Postgres service, as shown in the following image.

![Adding variables to Job Board in Railway](https://cms.railway.com/media/b82d1935583c85437a06f2d003bbdf6ff21f4a9f5958336672696ff102290cbb.png)

- Click **Deploy.**

Changing the environment variables will trigger automatic deployment. After the deployment finishes, you’ll see that the application is running successfully. 

### Access the application

To access the application, you need the service domain.

- Go to **job-board > Settings > Networking > Public Networking** and click **Generate Domain**.

After a few seconds, a random and unique domain will be assigned to your service. 

![Generate Domain for Job Board service in Railway](https://cms.railway.com/media/1cbe7fc44d9069f48ffb67a55e1ad2272600ce5378e0ef3622e2f9be576aad03.png)

You can now access the application on the internet using this URL. 

![Job Board accessible via public domain](https://cms.railway.com/media/187c30954112c8614134135493da13dc84b1a20f13a113eeececf7eeaa97d801.png)

## Set up a custom domain with Cloudflare

Now that the application is up and running on the internet it’s time to add a custom domain that is easy to remember and share. For this application, you’ll be using Cloudflare for DNS setup as well as the CDN proxy because it provides:

- **DDoS Protection**: Cloudflare offers robust DDoS mitigation, ensuring your app remains online even during attacks by filtering and absorbing malicious traffic.
- **Bot Detection**: It identifies and blocks harmful bots while allowing legitimate traffic, protecting against scraping and other bot-driven abuse.
- **Rate Limiting**: Controls excessive requests from specific users, preventing resource overuse or brute-force attacks.
- **CDN Caching**: Cloudflare caches static content globally, reducing server load on Railway, improving load times, and saving bandwidth costs.

To add a custom domain to your Railway application, follow these steps:

- On your Railway dashboard, go to **job-board > Settings > Networking > Public Networking** and click **+ Custom Domain**.

![Adding a Custom Domain to Job Board in Railway](https://cms.railway.com/media/5bde21784d4ef9ea09315c1e12caa8f9e355cb82c3802b84c3007eea76d6d85a.png)

- Enter the domain name.
- Select the port. You should choose the running application port suggested by Railway.
- Click **Add domain:** the **Configure DNS Records** dialog opens.
- Copy the CNAME value.
- Open your [Cloudflare dashboard](https://dash.cloudflare.com/).

![Cloudflare Dashboard](https://cms.railway.com/media/dc290340eabb7900cd2faa6f4d7ad954391d0a8a5ed9081a887d7548d8162da7.png)

- Go to **Site > DNS > Records.**
- Click **Add record.**
- In the **Type** field, select **CNAME.**
- In the **Name** field, enter the subdomain or `@` if you’re using root domain.
- In the **Target** field, paste the CNAME value copied earlier from the Railway dashboard.
- Keep the **Proxied** status on.
- Click **Save.**

To use Cloudflare proxy, you must activate end-to-end encryption. 

- Go to **SSL/TLS -> Overview,** click **Configure** and Select **Full** or **Full (strict)**.
To learn more, see [Provider specific instructions](https://docs.railway.com/networking/public-networking).

After saving all changes, your Railway service should detect the custom domain and Cloudflare proxy, as shown in the following image.

![Custom domain is detected in Railway](https://cms.railway.com/media/3f2de92623a512d8d70c8374b364d29436954c6268bbc0d9654e0fd8bce39bb5.png)

You can now access the application on your custom domain.

![Accessing Job Board from custom domain](https://cms.railway.com/media/a64ce604d002273f4590a403249bd0955d5b5b1eb56a95f54d2a3b48b995baf7.png)

## Conclusion

After completing this tutorial, you will have successfully built and deployed a SaaS application with Remix, PostgreSQL, and Railway. This application is a good starting point from which you can start your SaaS journey and build on top of it as you go. 

In the [next tutorial](https://railway.com/guides/scaling-a-saas-application), you’ll learn about scaling a SaaS using Railway, including cost management, multiple environments, and continuous deployments.