Build a poll-making website using SvelteKit, Turso, Drizzle, and deploy it to Vercel.

Build a poll-making website using SvelteKit, Turso, Drizzle, and deploy it to the edge using Vercel Edge Functions.

James SinkalaJames Sinkala
Cover image for Build a poll-making website using SvelteKit, Turso, Drizzle, and deploy it to Vercel.

According to StackOverflow's 2023 Developer Survey, Svelte is the most admired JavaScript web framework. To understand why many developers seem to admire this framework, we are going to examine how easy it is to deliver a regular web application experience using its streamlined framework, SvelteKit, together with Turso, the edge database, and my favorite ORM, Drizzle.

The website we're building in this tutorial “Turso at the Polls”, is a poll making website that simply lets you create and run polls online.

Each poll has a limit of 10 choices and a maximum running time of 10 days, 23 hours, and 59 minutes.

On completion, we are going to deploy the website to Vercel, specifically, on Vercel Edge Functions. Deploying the website to the Vercel's Edge Functions complements Turso, which is an edge database since we'll be placing both the compute and data close to users.

TLDR; the source-code to the final version of the application we are building [can be found on GitHub](https://github.com/tursodatabase/examples/blob/master/app-at-the-polls To proceed with the tutorial, make sure that you fulfill the following prerequisites.

Let's get down to building.

#Creating a new SvelteKit project

To get started, create a new SvelteKit project by running the following command.

npm create svelte@latest

Follow this template when responding to the resulting CLI prompt.

┌  Welcome to SvelteKit!
│
◇  Where should we create your project?
│  turso-at-the-polls
│
◇  Which Svelte app template?
│  Skeleton project
│
◇  Add type checking with TypeScript?
│  Yes, using TypeScript syntax
│
◇  Select additional options (use arrow keys/space bar)
│  Add ESLint for code linting, Add Prettier for code formatting
│
└  Your project is ready!

✔ Typescript
  Inside Svelte components, use <script lang="ts">

✔ ESLint
  <https://github.com/sveltejs/eslint-plugin-svelte>

✔ Prettier
  <https://prettier.io/docs/en/options.html>
  <https://github.com/sveltejs/prettier-plugin-svelte#options>

Install community-maintained integrations:
  <https://github.com/svelte-add/svelte-add>

On completion, you should have a new project directory named “turso-at-the-polls”. cd into it cd turso-at-the-polls and install the project dependencies by running npm install.

Since we are going to style our website with Tailwind CSS, add the Svelte Tailwind adder by running the following commands.

# add tailwindcss adder
npx svelte-add@latest tailwindcss

# install dependencies
npm run install

#The website's pages

In our app, we are going to list all the polls inside the index page “/”, view poll details within a “/poll/[id]” page, add new polls inside a “/new” page, and be able to delete a poll inside a “/delete” page. These four pages will cover the create (C), read (R), and delete (D) operations in CRUD. I'll leave out the update (U) operation as a task for the reader to do on their own.

To take care of these three operations, we'll create 3 new pages within the SvelteKit project (the index page is the root “+page.svelte” file found inside “src/routes”). Add the following three directories with the respective Svelte component and server code files inside “src/routes”.

├── delete
│   ├── +page.server.ts
│   └── +page.svelte
├── new
│   ├── +page.server.ts
│   └── +page.svelte
└── poll
    └── [id]
        ├── +page.server.ts
        └── +page.svelte

For the home page component “routes/+page.svelte”, add the code found in this file.

In this page (line 6–7), we are getting the questions from the server and then proceed to listing them with the help of the “poll card” component when a non-empty result has been received, else we display an appropriate message.

Add the poll card component “poll-card.svelte” under “src/lib” which is being imported in this page. Inside this component add the code found in this file.

This component simply lays out poll information and links to their details page.

For the poll details page component “/routes/poll/[id]/+page.svelte”, add the code found in GitHub.

In this page (line 6-10), just like in the previous, we are fetching the question data from the server, we then compute to see if the poll has expired and proceed to laying out the component markup depending on the state of the poll.

Also, in the poll details page, we have forms that lets us vote for any of the listed choices.

Inside the “/new” page component “/routes/new/+page.svelte”, we have a form that lets us add new polls to the database.

And lastly, in the delete page component “/routes/delete/+page.svelte”, we have another form that lets visitors delete the polls that they have created by providing the poll's “delete ID”. The poll delete ID is usually shown together with the poll creation success message after a new poll has been created.

Copy the code found in the linked files to the respective page component files before proceeding. We are first going to configure the ORM and database before implementing the server code for the respective pages.

#Setting up Drizzle

Since we are going to use the Drizzle ORM to construct queries that will be issued to Turso, we'll need to install its dependencies by running the following commands.

pnpm add drizzle-orm @libsql/client
pnpm add -D drizzle-kit dotenv tsx

Beyond the Drizzle dependencies, we're installing the dotenv and tsx modules. These two will assist us with fetching environment variables from “.env” files and the execution of TypeScript code respectively during local development.

#Adding the database schema

Our project is going to need 3 tables, a “questions” table that will store the poll questions, a “choices” table that will store the questions' choices, and a “votes” table that will store the votes made to each choice.

Create a “/drizzle” directory at the project's root and add a schema.ts file. Inside the schema file add the following database schema.

import { relations, sql } from 'drizzle-orm';
import { index, integer, sqliteTable, text, uniqueIndex } from 'drizzle-orm/sqlite-core';

export const questions = sqliteTable(
  'questions',
  {
    id: text('id').primaryKey(),
    question: text('question').notNull(),
    deleteId: text('delete_id').notNull(),
    expireDate: integer('expire_date').notNull(),
    createdAt: integer('created_at')
      .notNull()
      .default(sql`(cast (unixepoch() as int))`)
  }
);

export const questionRelations = relations(questions, ({ many }) => ({
  choices: many(choices)
}));

export const choices = sqliteTable(
  'choices',
  {
    id: text('id').primaryKey(),
    choice: text('choice').notNull(),
    questionId: text('question_id')
      .notNull()
      .references(() => questions.id)
  }
);

export const choicesRelations = relations(choices, ({ one, many }) => ({
  question: one(questions, {
    fields: [choices.questionId],
    references: [questions.id]
  }),
  votes: many(votes)
}));

export const votes = sqliteTable(
  'votes',
  {
    id: text('id').primaryKey(),
    choiceId: text('choice_id')
      .notNull()
      .references(() => choices.id),
    country: text('country').notNull().default('unknown'),
    voterId: text('voterId').notNull(),
    createdAt: integer('created_at')
      .notNull()
      .default(sql`(cast (unixepoch() as int))`)
  }
);

export const votesRelations = relations(votes, ({ one }) => ({
  choice: one(choices, {
    fields: [votes.choiceId],
    references: [choices.id]
  })
}));

(The table index configuration has been removed from the schema in this code block to render it short. Open the schema file on GitHub to view it in full.)

After having created the Drizzle schema, we can then generate the SQLite migration that we'll be transferring to our database afterwards. We can streamline this task by adding the following package.json command.

"drizzle:generate": "drizzle-kit generate:sqlite - out ./drizzle/migrations - breakpoints - schema=./drizzle/schema.ts",

We can then generate the SQLite migrations by simply running npm run drizzle:generate.

This script should generate a new SQLite migration file under “drizzle/migrations” since that is the output — out directory chosen in the above command.

We'll also need to set up the Drizzle migration code that we are going to run later in order to transfer the schema to our Turso database. To do so, create a migrate.ts file inside the “/drizzle” directory and add the following code.

import 'dotenv/config';
import { migrate } from 'drizzle-orm/libsql/migrator';
import { drizzle } from 'drizzle-orm/libsql';
import { createClient } from '@libsql/client';

export const client = createClient({
  url: process.env.VITE_TURSO_DB_URL as string,
  authToken: process.env.VITE_TURSO_DB_AUTH_TOKEN as string
});

export const db = drizzle(client);

async function main() {
  try {
    await migrate(db, {
      migrationsFolder: 'drizzle/migrations'
    });
    console.log('Tables migrated!');
    process.exit(0);
  } catch (error) {
    console.error('Error performing migration: ', error);
    process.exit(1);
  }
}

main();

Add another package.json script with the following configuration to streamline the migration task too.

"drizzle:migrate": "tsx drizzle/migrate",

Later, we'll be able to run migrations by running npm run drizzle:migrate.

To visualize your Turso database using the Drizzle Studio feature, make sure you add the drizzle configuration file “drizzle.config.ts” in the project's root directory containing the following code.

import type { Config } from 'drizzle-kit';
import * as dotenv from 'dotenv';
dotenv.config();

export default {
  schema: './drizzle/schema.ts',
  out: './drizzle/migrations',
  driver: 'turso',
  dbCredentials: {
    url: process.env.VITE_TURSO_DB_URL as string,
    authToken: process.env.VITE_TURSO_DB_AUTH_TOKEN as string
  }
} satisfies Config;

#Setting up Turso

To set up the database for our project, start by creating one by running the following command.

turso db create turso-at-the-polls

This command will create a database with the name “turso-at-the-polls'' at a Turso location closest to us. To really place your data at the edge, make sure that you create replicas of your Turso database closest to your website's users by using the turso db replicate command. To see all the Turso supported locations, run the turso db locations command.

We'll need two credentials to work with the created database within our SvelteKit project, the database URL and authentication token.

But first, create the necessary environment variables file within the Svelte project. Create a “.env” file and add the following environment variable keys.

VITE_TURSO_DB_URL=
VITE_TURSO_DB_AUTH_TOKEN=

To get the database URL, run turso db show — url turso-at-the-polls, then copy the result and assign it to the VITE_TURSO_DB_URL variable inside the “.env” file.

For the database authentication token, run turso db tokens create turso-at-the-polls, then again copy the result and assign it to the VITE_TURSO_DB_AUTH_TOKEN variable inside the “.env” file.

Next, we'll create a Turso client, wrapped with Drizzle for query generation, which we'll be importing inside all the server page modules (”+page.server.ts” files). We'll add this client to the SvelteKit project as a server module by placing it under the “src/lib/server” directory; this will ensure the accessibility of this code within other server modules alone, in-turn keeping our credentials safe.

Inside the “src/lib/server” file, add a new “turso.ts” file pasting the following code inside it.

import { type LibSQLDatabase, drizzle } from 'drizzle-orm/libsql';
import { createClient } from '@libsql/client/http';
import * as schema from '../../../drizzle/schema';

export function tursoClient(): LibSQLDatabase<typeof schema> {
  const url = import.meta.env.VITE_TURSO_DB_URL?.trim();
  if (url === undefined) {
    throw new Error('VITE_TURSO_DB_URL is not defined');
  }

  const authToken = import.meta.env.VITE_TURSO_DB_AUTH_TOKEN?.trim();
  if (authToken === undefined) {
    if (!url.includes('file:')) {
      throw new Error('VITE_TURSO_DB_AUTH_TOKEN is not defined');
    }
  }

  return drizzle(
    createClient({
      url,
      authToken
    }),
    { schema }
  );
}

We can now add the logic to the page server modules for each of the pages that we created.

#Performing database transactions inside pages

Starting with the home page route, add the following code to the “src/routes/+page.server.ts” server module.

import type { PageServerLoad } from './$types';
import { tursoClient } from '$lib/server/turso';

export const load: PageServerLoad = async () => {
  const db = tursoClient();

  const questions = await db.query.questions.findMany({
    with: {
      choices: {
        with: {
          votes: true
        }
      }
    }
  });

  if (questions !== undefined) {
    return { questions };
  }

  return { questions: [] };
};

Inside this page server module, we are exporting a load function within which we are utilizing the Turso database client imported from the server module created earlier to query the “questions” table for available questions. Finally, we return the results to the page component.

Inside the page load functions of the poll creation “/new”, poll deletion “/delete”, and poll details “/poll/:id” routes we are initially making sure that a unique id has been assigned to the site visitor and stored as a cookie.

export const load: PageServerLoad = async ({ cookies }) => {
  const userId = cookies.get('userid');

  if (!userId) {
    cookies.set('userid', crypto.randomUUID(), { path: '/' });
  }
  // other code
};

This unique user ID will help us implement a minimal guard against multiple votes to the same poll by a single visitor. We'll see this later in the implementation of the poll voting logic.

Inside the load function of the poll details page “src/routes/poll/[id]/+page.server.ts”, on top of the user ID creation logic, we are searching the questions table for the question containing the id that is being passed as part of the route path. Afterwards, we return the result to the page component.

export const load: PageServerLoad = async ({
  params,
  cookies
}): Promise<{ question: Question & { hasVoted: boolean } } | { ok: boolean; message: string }> => {

  const { id }: { id?: string } = params;

  if (id === undefined) {
    return { ok: false, message: 'Poll id not provided' };
  }
  const db = tursoClient();

  const question = await db.query.questions.findFirst({
    where: (questions, { eq }) => eq(questions.id, id),
    with: {
      choices: {
        with: {
          votes: true
        }
      }
    }
  });

    // other code
};

In this page module, we are also checking if the user has already voted on this poll so that the right logic can be implemented in the page component.

For the “/new” poll creation server module “src/routes/new/+page.server.ts”, we are getting all the submitted form data inside an action function (using the date-and-time npm module to generate a unixepoch() time from the user configured poll duration selected). Afterwards, we insert the respective questions and choices table rows depending on the provided poll configuration.

On successful poll creation, we return a poll deletion ID which the visitor can later use to delete the polls that they have created.

And finally, inside the action function of the poll deletion server module “src/routes/delete/+page.server.ts”, we are getting the question deletion ID from the submitted form and proceed to using that value to delete the poll by deleting all of the related table rows.

#Deploying to Vercel Edge Functions

Since we'll be deploying the SvelteKit project to Vercel, let's add the responsible Svelte adapter for that.

npm install -D @sveltejs/adapter-vercel

Next, add the following adapter configuration to the svelte configuration file “/svelte.config.js”.

import adapter from '@sveltejs/adapter-vercel';

export default {
  kit: {
    adapter: adapter({
      runtime: 'edge'
    })
  }
};

We are setting the Vercel deployment runtime to “edge” in the above configuration so as to deploy our website to Vercel edge functions.

We'll be deploying the project to Vercel from our machine. To do so, install the Vercel CLI by running the following command.

npm install -g vercel@latest

Authorize the Vercel CLI to use your Vercel account by running the command vercel login, then follow the instructions until you've completed logging in.

After having authenticated the CLI, we'll first run vercel dev so that we can see a working local server and have an existing project on the Vercel dashboard that we can add environment keys to.

After running vercel dev here's a template of the prompt you'll expect to encounter.

Vercel CLI 32.1.0
? Set up and develop “~/playground/turso-at-the-polls”? [Y/n] y
? Which scope should contain your project? <vercel-user-name>
? Link to existing project? [y/N] n
? What's your project's name? turso-at-the-polls
? In which directory is your code located? ./
Local settings detected in vercel.json:
Auto-detected Project Settings (SvelteKit (v1)):
- Build Command: vite build
- Development Command: vite dev --port $PORT
- Install Command: `yarn install`, `pnpm install`, or `npm install`
- Output Directory: public
? Want to modify these settings? [y/N] n
🔗  Linked to <username>/turso-at-the-polls (created .vercel)
> Running Dev Command “vite dev --port $PORT”

> Ready! Available at http://localhost:3000

  VITE v4.4.9  ready in 2479 ms

  ➜  Local:   http://localhost:3000/
  ➜  Network: use --host to expose
  ➜  press h to show help

We should by now be able to preview the website locally by visiting “localhost:3000”.

On the Vercel dashboard, you should see a new project added.

Click on the project card on the dashboard, then, open the settings tab in the resulting page and access the “Environment Variables” page from the left-side navigation.

Copy all of the contents of your project's “.env” file, and while on the “Environment Variables” page focus on an input field and paste to automatically add the environment variables used within your project to Vercel.

Finalize this step by clicking the save button.

Back on your terminal, run vercel deploy to start the project's deployment process.

When this process has completed, you should be able to see the production link to your deployed website on the terminal's log.

$ vercel deploy
Vercel CLI 32.1.0
🔍 Inspect: https://vercel.com/<username>/turso-at-the-polls/<uuid> [4s]
✅ Production: https://at-the-polls.vercel.app [3m]
📝 Deployed to production. Run `vercel --prod` to overwrite later (https://vercel.link/2F).
💡 To change the domain or build command, go to https://vercel.com/<username>/turso-at-the-polls/settings

Open the link on your browser to see your live website.

You can now visit your website and create some polls.

For more information on the technologies used in this tutorial, make sure to visit the following links:

If you love reading more tutorials like this, follow me on Twitter @xinnks. You can also send me feedback and suggestions on other kinds of tutorials or frameworks you'd like to see me cover next.

scarf