DEV Community

Cover image for How to Build and Deploy Full-Stack JavaScript Apps with NextJS, Tailwind, PostgreSQL, and Sevallaβš‘πŸ‘¨β€πŸ’»
Madza
Madza Subscriber

Posted on • Originally published at madza.hashnode.dev

How to Build and Deploy Full-Stack JavaScript Apps with NextJS, Tailwind, PostgreSQL, and Sevallaβš‘πŸ‘¨β€πŸ’»

In the rapidly evolving digital landscape with so many modern technologies being introduced every day, managing the app infrastructure might seem as a daunting task.

Besides tight deadlines and high expectations from your clients, stuff like setting up, configuring, and maintaining servers, databases, and static sites can feel like a seperate job itself.

That often requires a significant amount of time, resources, and expertise, potentially delaying the project due to unexpected struggles and challenges.

Sevalla was created as a solution to solve this, allowing developers to focus on the stuff that matters the most, like working on new ideas for the project and coding itself.

In this tutorial, we will first take a look at what Sevalla is, review its main features, and see how it could be useful. Then we will build a demo app with NextJS and deploy it on Sevalla.

Then we will create a PostgreSQL database on Sevalla and connect it to our NextJS app, so you get to see how Sevalla can be used to manage and host full-stack projects.

Thanks to the Sevalla team for sponsoring this article.


What is Sevalla?

Sevalla is a flexible Platform as a Service (PaaS) that simplifies hosting applications, databases, and static sites across many global data centers with its intuitive cloud interface.

It eliminates complex infrastructure management, allowing developers to focus on coding while enjoying seamless deployment experiences with robust security and scalability.

Sevalla

Whether you're building a blog, a web app, or a database-driven service, Sevalla handles your deployments, logs, and analytics so you can focus on developing your ideas and businesses.

The platform offers a clean and well-thought user interface as well comes with great documentation that allows you to handle complex DevOps infrastructures with ease.

Main features and why it is useful?

Sevalla offers to deploy applications on 25 global data centers with 260+ Cloudflare PoP servers, reducing latency and improving performance.

All your projects will be backed up with robust security via Cloudflare DDoS protection, private network connectivity, and strong role-based access control to keep them secure.

Main features

Users can perform application updates with Kubernetes health checks to ensure that their apps are always running, with instant rollbacks if needed.

Sevalla also supports flexible scaling with unlimited users & resources, parallel builds, no limiting feature gates, and auto-scaling depending on the application usage.

Additional cool features include support for Git & Docker, public/private repositories, auto-environment setup, and zero infrastructure management for seamless hosting.

What we will be building?

In order to test the features of Sevalla in practice, we will first need to build a demo application.

We will start by making the layout of the application, so we know what components we need to code and how they will be nested in the app structure.

The application layout will look like in the diagram below:

Application layout

At the top of the application layout, there will be a TodoStats component that will show the user how many tasks there are in total and how many of them are currently completed.

There will be a TodoForm component below it, that the user will use to add a new task to the list. It will consist of the input area for text and the button to save the entry.

All of the entries will be saved as TodoCard components. They will consist of a clickable checkbox to track the completion state, the task text, as well as the edit and delete buttons.

Notice the TodoCard components will be stacked on top of each other with the recent ones being positioned on the top. After deleting any of them the stack will adjust itself.

Creating a new NextJS app

First, we will need to create the basis for our application. We will use the create-next-app boilerplate tool, that will install everything required for our application.

Run the command npx create-next-app sevalla-demo. This will start the setup wizard in the terminal. Configure everything as shown below:

Setup wizard

Wait a minute while the installation completes. Now change the working directory into the newly created project by running cd sevalla-demo.

Let's check if our application works as expected. Run npm run dev command in your terminal. This should start the development server for you.

You should be presented with the NextJS landing page in your default browser. If now open it manually and navigate to http://localhost:3000, which should look like this:

NextJS landing page

While we are at the initialization stage of the project, let’s perform some style resetting so we can build the app design from the ground up.

Open the file globals.css inside the app folder and leave just the base Tailwind imports.

@tailwind base;
@tailwind components;
@tailwind utilities;
Enter fullscreen mode Exit fullscreen mode

Also, while in the app directory, open the file layout.js, and change the code to the following:

import { Montserrat } from "next/font/google";
import "./globals.css";

const montserrat = Montserrat({
  subsets: ["latin"],
});

export const metadata = {
  title: "My task manager",
  description: "Manage tasks and prioritize your daily activities efficiently",
};

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body className={`${montserrat.className}`}>{children}</body>
    </html>
  );
}
Enter fullscreen mode Exit fullscreen mode

This will set our app metadata such as title and description as well as update the font to the Montserrat from Google fonts that will be a better fit for our app design.

Creating layout components

The layout components will be the building blocks for the user interface.

We will create components based on the layout wireframe we created earlier, to keep our code well organized and separate the logic from the layout.

You can do this manually, but we will use the terminal to quickly create all of the necessary folders and files for the components of our application.

Open your terminal and run the following command:

mkdir components && cd components && touch TodoCard.js TodoForm.js TodoStats.js
Enter fullscreen mode Exit fullscreen mode

After the file structure has been created, navigate to the newly created folder and populate each of the files with the code as shown below:

TodoCard.js

"use client";

import { useState } from "react";

function TodoCard({ todo, onDelete, onEdit, onToggle }) {
  const [isEditing, setIsEditing] = useState(false);

  const handleSubmit = (e) => {
    e.preventDefault();
    const form = e.currentTarget;
    const titleInput = form.querySelector('input[name="title"]');

    onEdit(todo.id, {
      title: titleInput.value,
    });

    setIsEditing(false);
  };

  if (isEditing) {
    return (
      <form onSubmit={handleSubmit} className="p-4">
        <input
          type="text"
          name="title"
          defaultValue={todo.title}
          className="mb-2 w-full rounded border p-2"
        />
        <div className="flex justify-end gap-2">
          <button
            type="button"
            onClick={() => setIsEditing(false)}
            className="rounded px-4 py-2 text-gray-600 hover:bg-gray-100"
          >
            Cancel
          </button>
          <button
            type="submit"
            className="rounded bg-yellow-500 px-4 py-2 text-white hover:bg-yellow-600"
          >
            Save
          </button>
        </div>
      </form>
    );
  }

  return (
    <div className="rounded-lg p-4">
      <div className="flex items-center justify-between">
        <div className="flex items-center gap-4">
          <button
            onClick={() => onToggle(todo.id)}
            className={`relative h-6 w-6 shrink-0 cursor-pointer rounded-md border-2 border-gray-300 hover:border-gray-400 ${
              todo.completed ? "bg-green-500" : "bg-white"
            }`}
          >
            <span className="absolute left-1/2 top-1/2 block h-3 w-3 -translate-x-1/2 -translate-y-1/2 text-[10px] text-white">
              βœ“
            </span>
          </button>
          <h3
            className={`font-medium ${
              todo.completed ? "text-gray-400 line-through" : "text-gray-900"
            }`}
          >
            {todo.title}
          </h3>
        </div>
        <div className="flex items-center gap-2">
          <button
            onClick={() => setIsEditing(true)}
            className="rounded bg-yellow-500 px-3 py-1.5 text-sm font-medium text-white hover:bg-yellow-600"
          >
            Edit
          </button>
          <button
            onClick={() => onDelete(todo.id)}
            className="rounded bg-red-500 px-3 py-1.5 text-sm font-medium text-white hover:bg-red-600"
          >
            Delete
          </button>
        </div>
      </div>
    </div>
  );
}

export default TodoCard;
Enter fullscreen mode Exit fullscreen mode

We first created a isEditing state to track whether or not the Card is in editing mode.

If the task Card is in editing mode, we display the layout with the input area for the task title and Cancel and Save buttons to discard and proceed with changes respectively.

Otherwise, if the Card is not in the editing mode, then we display a clickable checkbox to track the completion state, the task text, as well as the Edit and Delete buttons.

We created the handleSubmit function to describe the action when the user saves an edit. The user input is getting retrieved and the data is sent as onEdit prop.

Similarly, for the checkbox to track the completion state of the task we created onToggle prop and onDelete prop for the Delete button to remove the task.

TodoForm.js

"use client";

import { useState } from "react";

export default function TodoForm({ onAdd }) {
  const [isOpen, setIsOpen] = useState(false);

  const handleSubmit = (e) => {
    e.preventDefault();
    const form = e.currentTarget;
    const titleInput = form.querySelector('input[name="title"]');

    onAdd({
      title: titleInput.value,
      completed: false,
    });

    form.reset();
    setIsOpen(false);
  };

  return (
    <div className="mb-6">
      {!isOpen ? (
        <button
          onClick={() => setIsOpen(true)}
          className="flex w-full items-center justify-center gap-2 rounded-lg bg-green-500 p-4 text-white hover:bg-green-600"
        >
          <span className="text-2xl">+</span> Add New Task
        </button>
      ) : (
        <form onSubmit={handleSubmit} className="bg-white p-4">
          <input
            type="text"
            name="title"
            placeholder="Task title"
            required
            className="mb-2 w-full rounded border p-2"
          />
          <div className="flex justify-end gap-2">
            <button
              type="button"
              onClick={() => setIsOpen(false)}
              className="rounded px-4 py-2 text-gray-600 hover:bg-gray-100"
            >
              Cancel
            </button>
            <button
              type="submit"
              className="rounded bg-green-500 px-4 py-2 text-white hover:bg-green-600"
            >
              Add Task
            </button>
          </div>
        </form>
      )}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

We created a Form to add new tasks, and implement an isOpen state to track whether or not the user is in the process of adding a new item to the list.

If the form is opened, we will render an input area with a placeholder to make a new entry and the Cancel and Save buttons to discard or proceed with the new task.

However, if the form is not opened, we will display the Add new task button, which sets the isOpen state to true when the user clicks on it.

We also created a handleSubmit function so the app can collect the user input, change the open state to false and send that data as onAdd prop when the user saves the entry.

TodoStats.js

export default function TodoStats({ todos }) {
  const completedTodos = todos.filter((todo) => todo.completed).length;

  return (
    <div className="mb-6 flex justify-between rounded-lg bg-white p-4">
      <div className="flex flex-col items-center">
        <p className="text-md text-gray-600">Total Tasks</p>
        <p className="text-2xl font-bold">{todos.length}</p>
      </div>
      <div className="flex flex-col items-center">
        <p className="text-md text-gray-600">Completed</p>
        <p className="text-2xl font-bold text-green-600">{completedTodos}</p>
      </div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

The Stats component will be used to display how many total tasks there are in the list and how many of them are already completed.

We divided the parent wrapper into two columns using flexbox layout and gave a justify-between property for both stats to be displayed on the opposite sides of the component.

Putting together the frontend logic

In this section, we will create the frontend logic by putting all of the components we created earlier together and providing the necessary props for them to work as expected.

Navigate back to the app folder and edit the code for the page.js file as shown below.

"use client";

import { useState } from "react";
import TodoStats from "@/components/TodoStats";
import TodoForm from "@/components/TodoForm";
import TodoCard from "@/components/TodoCard";

const initialTodos = [
  {
    id: 1,
    title: "plan weekend family trip",
    completed: false,
  },
  {
    id: 2,
    title: "fix and clean the bike",
    completed: false,
  },
  {
    id: 3,
    title: "buy fruits and groceries",
    completed: true,
  },
];

export default function Home() {
  const [todos, setTodos] = useState(initialTodos);

  const addTodo = (newTodo) => {
    setTodos((prev) => [
      {
        ...newTodo,
        id: todos.reduce((maxId, todo) =>  Math.max(maxId, todo.id), 0) +  1,
      },
      ...prev,
    ]);
  };

  const editTodo = (id, updates) => {
    setTodos((prev) =>
      prev.map((todo) => (todo.id === id ? { ...todo, ...updates } : todo))
    );
  };

  const deleteTodo = (id) => {
    setTodos((prev) => prev.filter((todo) => todo.id !== id));
  };

  const toggleTodo = (id) => {
    setTodos((prev) =>
      prev.map((todo) =>
        todo.id === id ? { ...todo, completed: !todo.completed } : todo
      )
    );
  };

  return (
    <div className="mx-auto max-w-2xl p-4">
      <TodoStats todos={todos} />
      <TodoForm onAdd={addTodo} />
      <div className="space-y-4">
        {todos.map((todo) => (
          <TodoCard
            key={todo.id}
            todo={todo}
            onDelete={deleteTodo}
            onEdit={editTodo}
            onToggle={toggleTodo}
          />
        ))}
      </div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

We first imported all of the components we created in the previous chapter.

Then we defined a variable initialTodos to simulate some sample data for testing purposes. Later we will create a backend logic and fetch the data from the database.

After that, we created four functions addTodo, editTodo, deleteTodo and toggleTodo to perform the CRUD operations for the sample data we provided.

Finally, we rendered all of the components in the order we designed in the application layout wireframe and passed all of the required data to their props.

Testing the user interface

At this point, we have completed the work on the frontend for the application. Let's test it out if the app is now looking as we expected it to be in the wireframe.

Check if your developer server is still running. If not, make sure to set the working directory to the root and run the npm run dev again.

Open your web browser at http://localhost:3000 and you should be presented with a user interface, that is straightforward to use and covers all of the necessary features to work with to-do lists.

User interface

However, our data is not stored anywhere, meaning we cannot access it outside the dev server.

We will fix this later in the tutorial by turning the project into a full-stack app.

Creating the connection pool

Our work on the application backend will start by creating the entry point which frontend will use to communicate with the database hosted on Sevalla.

We will use the pg utility that will help us to configure this with a few lines of code.

Run npm install pg in your terminal.

This will install a PostgreSQL client package that will allow us to connect with the database in the later stages of the tutorial.

Next, create a new folder lib in the project root and create a new file db.js inside of it.

Include the following code as shown below:

import { Pool } from "pg";

const pool = new Pool({
  host: process.env.DB_HOST,
  url: process.env.DB_URL,
  port: process.env.DB_PORT,
  database: process.env.DB_DATABASE,
  user: process.env.DB_USERNAME,
  password: process.env.DB_PASSWORD,
});

export async function query(text, params) {
  const client = await pool.connect();
  try {
    const result = await client.query(text, params);
    return result;
  } finally {
    client.release();
  }
}
Enter fullscreen mode Exit fullscreen mode

We first imported the Pool class from the pg library.

Then we created a new Pool instance to source the configuration parameters from env variables. We will later configure Sevalla to source these variables from their system.

Finally, in the query function we first acquired the client from the connection pool, then executed the query, and after that performed the resource cleanup.

Defining the CRUD operations

After we have configured the access point for the database, we need to define what the backend needs to do when the application communicates with it.

We will write a few SQL queries, separating the functionality for each CRUD operation.

Create actions.js file inside the app folder and include the following code:

"use server";

import { query } from "@/lib/db";

export async function getTodos() {
  try {
    const result = await query("SELECT * FROM todos ORDER BY created_at DESC");
    return result.rows;
  } catch (error) {
    console.error("Error fetching todos:", error);
    throw new Error("Failed to fetch todos");
  }
}

export async function addTodo(formData) {
  try {
    const title = formData.title;
    const result = await query(
      "INSERT INTO todos (title) VALUES ($1) RETURNING *",
      [title]
    );

    return result.rows[0];
  } catch (error) {
    console.error("Error adding todo:", error);
    throw new Error("Failed to add todo");
  }
}

export async function updateTodo(id, updates) {
  try {
    const result = await query(
      "UPDATE todos SET title = $1 WHERE id = $2 RETURNING *",
      [updates.title, id]
    );

    return result.rows[0];
  } catch (error) {
    console.error("Error updating todo:", error);
    throw new Error("Failed to update todo");
  }
}

export async function toggleTodo(id) {
  try {
    const result = await query(
      "UPDATE todos SET completed = NOT completed WHERE id = $1 RETURNING *",
      [id]
    );

    return result.rows[0];
  } catch (error) {
    console.error("Error toggling todo:", error);
    throw new Error("Failed to toggle todo");
  }
}

export async function deleteTodo(id) {
  try {
    await query("DELETE FROM todos WHERE id = $1", [id]);
  } catch (error) {
    console.error("Error deleting todo:", error);
    throw new Error("Failed to delete todo");
  }
}
Enter fullscreen mode Exit fullscreen mode

We first imported the query function from the connection pool we created earlier so we could send the requests to the database.

After that, we created getTodos function that will fetch the data on the initial launch of the app and each time we update any of the data.

Then we created addTodo, updateTodo, toggleTodo and deleteTodo functions for other CRUD operations.

Each of the functions was structured in try and catch blocks so we can log errors if any of the operations fail to complete successfully.

Updating the app logic with data

Finally, we will put all of the functions we created earlier together and update the app logic, so we would be all set to work with the database.

In this stage, we will configure the application to replace the sample data with the data coming from the database which we will connect later.

Open the page.js file in the app folder and edit the existing code as shown below:

"use client";

import { useEffect, useState } from "react";
import TodoStats from "@/components/TodoStats";
import TodoForm from "@/components/TodoForm";
import TodoCard from "@/components/TodoCard";
import {
  getTodos,
  addTodo,
  updateTodo,
  deleteTodo,
  toggleTodo,
} from "./actions";

function Home() {
  const [todos, setTodos] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    loadTodos();
  }, []);

  async function loadTodos() {
    try {
      const data = await getTodos();
      setTodos(data);
      setError(null);
    } catch (err) {
      setError("Failed to load todos");
      console.error("Error loading todos:", err);
    } finally {
      setLoading(false);
    }
  }

  async function handleAddTodo(formData) {
    try {
      await addTodo(formData);
      await loadTodos();
    } catch (err) {
      setError("Failed to add todo");
      console.error("Error adding todo:", err);
    }
  }

  async function handleEditTodo(id, updates) {
    try {
      await updateTodo(id, updates);
      await loadTodos();
    } catch (err) {
      setError("Failed to update todo");
      console.error("Error updating todo:", err);
    }
  }

  async function handleDeleteTodo(id) {
    try {
      await deleteTodo(id);
      await loadTodos();
    } catch (err) {
      setError("Failed to delete todo");
      console.error("Error deleting todo:", err);
    }
  }

  async function handleToggleTodo(id) {
    try {
      await toggleTodo(id);
      await loadTodos();
    } catch (err) {
      setError("Failed to toggle todo");
      console.error("Error toggling todo:", err);
    }
  }

  if (loading) {
    return <div className="text-center p-4">Loading...</div>;
  }

  if (error) {
    return (
      <div className="text-center p-4 text-red-500">
        {error}
        <button onClick={loadTodos} className="ml-2 underline">
          Retry
        </button>
      </div>
    );
  }

  return (
    <div className="mx-auto max-w-2xl p-4">
      <TodoStats todos={todos} />
      <TodoForm onAdd={handleAddTodo} />
      {error && <div className="mb-4 text-center text-red-500">{error}</div>}
      <div className="space-y-4">
        {todos.map((todo) => (
          <TodoCard
            key={todo.id}
            todo={todo}
            onDelete={handleDeleteTodo}
            onEdit={handleEditTodo}
            onToggle={handleToggleTodo}
          />
        ))}
      </div>
    </div>
  );
}

export default Home;
Enter fullscreen mode Exit fullscreen mode

First, we imported all of the functions we created in the previous section.

Then we executed the loadTodos function to fetch the data from the database the first time the application loads. We stored the fetched data in the todos state variable.

For each of the CRUD features we created a seperate function to either create, read, update, or delete and load the new data after the database has been updated.

Finally, we rendered all of the imported frontend components we created earlier and passed the data coming from the database into the todos state variable as props.

We also created loading and error state variables so we can render an appropriate message for the user while the data is being fetched or there is an error.

Pushing code to GitHub

We will push our code to a remote Github repository so we have a build source for Sevalla.

The first thing we need will be Git, so we can communicate with GitHub.

Navigate to their downloads and the installation wizard should be pretty straightforward.

Next, create a new GitHub account if you already do not have one.

Navigate to the sign up page and create a free account by providing your email.

GitHub signup

After that, log in and select 'Create a new repository' from the top menu.

New repository

This will give you a form of repository details. Provide the repository name, and description (optional), and choose whether you want your repository to be private or public.

Run the following command in your terminal:

git init
git add .
git commit -m "sevalla-demo"
git branch -M main
git remote add origin https://github.com/madzadev/sevalla-demo.git
git push -u origin main
Enter fullscreen mode Exit fullscreen mode

Now open your GitHub repository in a web browser, refresh it and you should see our application being successfully pushed to it.

Pushed repository

Creating the app on Sevalla

In this section, we will create a new application on Sevalla, so we can later configure it to access our application from anywhere on the web.

Create a new account on Sevalla if you do not already have one. You can sign up using your GitHub, Gitlab, Bitbucket, or using your email.

Sevalla signup

After your new account has been created you will be presented with the main dashboard. The application is divided into three blocks - applications, static sites, and databases.

Sevalla dashboard

Navigate to the 'Applications' section and click on 'Add application'.

This should open a new form to provide the source of the application. Since we already pushed the application code on GitHub we will use the repository as a source.

Sevalla app source

You can choose from 25 data center locations, which allows you to place your application in a geographical location closest to your visitors.

Also, you can select a pod size for your application's web process according to your needs.

After you have filled out the app deployment form, click on β€˜Createβ€˜ and you should be redirected to the main dashboard of your application where you can see its infrastructure overview.

Create new Sevalla app

Notice, that Sevalla also gave you a random Kinsta domain after your initial deployment. You can keep it or set up your custom domain by visiting Domain settings.

Creating the database on Sevalla

Next, we will create a new database where the data of our application will be stored.

First, navigate to the 'Database' section on Sevalla.

Click on 'Add database'. It should open a form to fill out the database parameters you want to create, including database type, version, name, location, and allocated resources.

Add Sevalla database

For this tutorial, we will be creating a PostgreSQL version 17 instance.

Same as for the application deployment, select the data center location closest to where most of the users of your application will be located and pick a sufficient resources type.

After the database has been created you will be redirected to the database dashboard where you can see all of the access parameters for the database.

Sevalla database dashboard

Make sure to enable the β€˜External connectionβ€˜ toggle if it was not already, since we will need that access in the next section of the tutorial.

Creating the database table

In this section, we will make a new table in the database we just created on Sevalla.

We will need a database GUI software TablePlus, so we can work with the database using a visual interface. It comes with a free trial that will be more than enough for our needs.

Navigate to their downloads and install it by following the instructions.

Then open it, select 'Create a new connection', and pick PostgreSQL.

TablePlus connections

That will open a modal to enter the access data from the Sevalla database page we retrieved in the previous section. We will make an external connection since we are using 3rd party software.

TablePlus configuration

Once it is done, click on 'Connect' and you should land in an empty workspace since we do not have any data in our database yet.

We will need to create a new database table so our data is structured and well organized.

Open the SQL editor from the top navigation panel and you should be presented with a terminal-like interface where you can write SQL queries.

TablePlus SQL editor

Paste the following query inside the editor, and it will create a new database table for storing the data.

CREATE TABLE todos (
        id SERIAL PRIMARY KEY,
        title VARCHAR(255) NOT NULL,
        completed BOOLEAN DEFAULT FALSE,
        created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
      );
Enter fullscreen mode Exit fullscreen mode

Refresh the app and you should now see a new database table todos with four columns.

The id will be the primary key, the title will be the title of the task, completed will be a boolean for the completion and created_at will be the timestamp of the creation.

Connecting the app to the database

Navigate back to the Sevalla and in this step, we will connect the application to the database.

Since we already created and hosted both the app and the database on Sevalla, we will be creating the internal connection, which is the recommended way and best for performance.

If you were to host the app elsewhere and host just the database on Sevalla, you could do that by using an external connection, but the communication time between both would be longer.

Open up the Sevalla and navigate to the 'Databases', and under the 'Connected applications' click on 'Add internal connection'. This should open up a modal form with connection options.

Add internal connection

In the app dropdown select the application we deployed on Sevalla before.

Also, make sure to enable the toggle for 'Add environment variables to the application', as it will provide all of the environment variables we need to connect.

Add environment variables to the application

Click 'Add connection' and your frontend app should now be successfully connected to the database.

Deploying the application on Sevalla

At this point, we have configured and connected both the frontend and backed and the last thing left to do is to deploy the application so we can access it live on the web.

Navigate to the β€˜'Applications’ section and click on the β€˜Deploymentsβ€˜ tab.

Sevalla deployments

Click on β€˜Deploy now’ and you should get the modal asking what branch you want to deploy. We only have one branch, but if you are working on new features in the future, it’s useful to have this option.

Sevalla Deploy now

Wait for a couple of minutes for Sevalla to complete the deployment and you should receive a notification for successful completion, marked by the green tick next to the deployment record.

Complete the deployment

If for some reason your deployment was not successful, you can click on each deployment to read the full log and see what caused that. After fixing it, click Redeploy and you should be fine!

Testing the live application

Finally, let's test our application in practice. We will be interacting with the user interface and simultaneously check on backend if data is working as expected.

The best way to test is performing to create, read, update, and delete operations, as they are the backbone for working with data in any full-stack application.

Clicking on β€˜Visit appβ€˜ in the Deployments section and if your deployment section was successful you should now be able to see the app live on the web.

First, let's add a few new tasks using our user interface.

Add new tasks

Now, switch back to the database and notice the new records have been successfully added to our PostgreSQL instance hosted on Sevalla.

DB test1

Next, how about we find a mistake or simply would like to edit a few records? Try to do just that by fixing any of the items in the list.

Edit tasks

Check back in our database and notice that the record has indeed been updated.

DB test2

During the day we are starting to complete the list which is the main purpose of the application, so let's test if we can eliminate some of the tasks.

Complete tasks

Open the database table and check if the completion state boolean on the items you ticked has changed to true. We are noticing just that, great!

DB test3

Finally, how about we delete some of the records we completed just so we do not collect a pile of archived actions? Each task we remove should disappear from the list.

Delete tasks

Again, check if the backend mirrors what we just saw on the UI. The records we removed are gone from the database as well, which is what we wanted.

DB test4

At this point, you have created a fully functional full-stack application. We just tested that the UI in the live app works and the data changes are reflected in the remote database.

This means you could now access and work on your app from anywhere in the world from any device with internet access and your data would be saved. Congratulations!

Conclusion

In this tutorial, we worked with lots of technologies like NextJS, Tailwind, PostgreSQL, Git, GitHub, and Sevalla. During the process, we built a practical full-stack demo application.

We used NextJS as a base framework for our application, Tailwind for styling, PostgreSQL for storing our data, Git to push our code to GitHub, and Sevalla to host everything.

We learned that Sevalla can be used not only for hosting applications and static sites but also for databases, meaning it provides everything you need to host modern apps.

The user interface of Sevalla was very straightforward and well thought out. Each concept was well separated meaning both new and existing users will have clarity in their workflow.

Hopefully, it will come in handy for your future projects whenever you need to manage or host your static sites, apps, or databases. Sign up now and explore all of the features yourself!


Writing has always been my passion and it gives me pleasure to help and inspire people. If you have any questions, feel free to reach out!

Make sure to receive the best resources, tools, productivity tips, and career growth tips I discover by subscribing to my newsletter!

Also, connect with me on Twitter, LinkedIn, and GitHub!

Top comments (12)

Collapse
 
andrewbaisden profile image
Andrew Baisden

Great tutorial!

Collapse
 
madza profile image
Madza

Thanks a lot, mate! πŸ‘πŸ’―

Collapse
 
devshefali profile image
Shefali

Great article, Madza!

Collapse
 
madza profile image
Madza

Thank you so much, Shefali! Glad you liked it!

Collapse
 
keyru_nasirusman profile image
keyru Nasir Usman

I usually want to manage servers by myself because It is very cheap and very reliable. But cost is the main reason.

Collapse
 
madza profile image
Madza

Thanks for the insight and checking out the article!

Collapse
 
riya_marketing_2025 profile image
Riya

Great Post

Collapse
 
madza profile image
Madza

Thank you, means a lot!

Collapse
 
kwnaidoo profile image
Kevin Naidoo

Nice! I like your dashboard, very clean and uncluttered. I prefer managing servers myself, but if I ever need to host a small project, I will definitely try this.

Collapse
 
madza profile image
Madza

Thanks a lot for the feedback, appreciated it! And I hope it's useful for you!

Collapse
 
devluc profile image
Devluc

Great article Madza. Sevalla looks like an awesome tool. Thanks for sharing it

Collapse
 
madza profile image
Madza

Thanks for checking it out and my pleasure!