Appwrite Hand-In-Hand with Svelte Kit (SSR)

Appwrite Hand-In-Hand with Svelte Kit (SSR)

In this article, we will build Almost Casino, a web application that allows you to sign in and play coin flip with virtual currency.

ยท

10 min read

If you are here only to see how to make SSR work, you can jump into โšก Server Side Rendering and ๐Ÿš€ Deployment sections. They explain it all. You can also check out the GitHub repository.


Let's build a project!

In this article, we will build Almost Casino, a web application that allows you to sign in and play coin flip with virtual currency called Almost Dollar.

As you can see, not the brightest idea... The point of the project is not to build a 1B$ company in a weekend, instead, to showcase how we can achieve server-side rendering with Appwrite backend.

๐Ÿ–ผ๏ธ Pics, pics, pics!

Let's see what we are going to build ๐Ÿ˜Ž

Almost Casino showcase

A single page that shows the ID of the currently logged-in user, his current balance, and a coin flip game. When playing coin flip, you can configure a bet and pick which side of the coin you would like to bet on. For the sick of simplicity, there will be no coin-flipping animation. Do you know what that means? ๐Ÿ‘€ YOU can add the cool-looking animation!

๐Ÿ“ƒ Requirements

We will be using multiple technologies in this demo application, and having a basic understanding of them will be extremely helpful.

Regarding the JavaScript framework, we will be using a simple and fun framework Svelte. To allow routing and introduce better folder structure as well as the possibility of SSG and SSR, we are going to use Svelte Kit.

To give our application some cool styles, we will use TailwindCSS, a CSS library for rapid designing.

Instead of writing our own backend from scratch, we will be using backend-as-a-service Appwrite to build and deploy server logic easily.

Last but not least, we will use Vercel as our static.

๐Ÿ› ๏ธ Create Svekte Kit Project

The whole Almost Casino application is open-sourced and can be found in GitHub repository. The app is also live on the app.almost-casino.matejbaco.eu.

For those who follow along, let's create a new Svelte Kit project:

$ npm init svelte almost-casino
โœ” Which Svelte app template? โ€บ Skeleton project
โœ” Add type checking? โ€บ TypeScript
โœ” Add ESLint for code linting? โ€ฆ No / Yes
โœ” Add Prettier for code formatting? โ€ฆ No / Yes
โœ” Add Playwright for browser testing? โ€ฆ No / Yes

Next, let's enter our new project and run the development server:

$ cd almost-casino
$ npm install
$ npm run dev

We now have the Svelte Kit web application running and listening on localhost:3000 ๐Ÿฅณ

Let's make sure to follow Tailwind installation instructions to install it into our newly created Svelte Kit project.

Appwrite backend setup

If you don't have the Appwrite server running yet, please follow Appwrite installation instructions.

Once the server is ready, let's create an account and a project with the custom ID almostCasino. Next, we go into the Database section and create a collection with ID profiles. In the setting of this collection, we set collection-level permission with read access to role:member, and write access empty. Finally, we add the float attribute balance and mark it as required.

๐Ÿค– Appwrite Service

Let's start by creating a .env file and putting information about our Appwrite project in there:

VITE_APPWRITE_ENDPOINT=http://localhost/v1
VITE_APPWRITE_PROJECT_ID=almostCasino

Before coding the application, let's create a class that will serve as an interface for all communication with our Appwrite backend. Here we will have methods for authentication, managing profile, and playing the coin flip game. Let's create new file src/lib/appwrite.ts with all of the methods:

import { Appwrite, type RealtimeResponseEvent, type Models } from 'appwrite';

const appwrite = new Appwrite();
appwrite
  .setEndpoint(import.meta.env.VITE_APPWRITE_ENDPOINT)
  .setProject(import.meta.env.VITE_APPWRITE_PROJECT_ID);

export type Profile = {
  balance: number;
} & Models.Document;

export class AppwriteService {
  // SSR related
  public static setSSR(cookieStr: string) {
    const authCookies: any = {};
    authCookies[`a_session_${import.meta.env.VITE_APPWRITE_PROJECT_ID}`] = cookieStr;
    appwrite.headers['X-Fallback-Cookies'] = JSON.stringify(authCookies);
  }

  // Authentication-related
  public static async createAccount() {
    return await appwrite.account.createAnonymousSession();
  }

  public static async getAccount() {
    return await appwrite.account.get();
  }

  public static async signOut() {
    return await appwrite.account.deleteSession('current');
  }

  // Profile-related
  public static async getProfile(userId: string): Promise<Profile> {
    const response = await appwrite.functions.createExecution('createProfile', undefined, false);
    if (response.statusCode !== 200) { throw new Error(response.stderr); }

    return JSON.parse(response.response).profile;
  }

  public static async subscribeProfile(userId: string, callback: (payload: RealtimeResponseEvent<Profile>) => void) {
    appwrite.subscribe(`collections.profiles.documents.${userId}`, callback);
  }

  // Game-related
  public static async bet(betPrice: number, betSide: 'tails' | 'heads'): Promise<boolean> {
    const response = await appwrite.functions.createExecution('placeBet', JSON.stringify({
      betPrice,
      betSide
    }), false);

    if (response.statusCode !== 200) { throw new Error(response.stderr); }

    return JSON.parse(response.response).didWin;
  }
}

With this in place, we have everything ready to have proper communication between our Svelte Kit application and our Appwrite backend ๐Ÿ’ช

You can notice we execute some functions in this class, for instance, createProfile or placeBet. We use Appwrite Functions for these actions to keep them secure and not allow clients to make direct changes to their money balance. You can find the source code of these functions in GitHub repository.

๐Ÿ” Authentication Page

We start by creating a button to create an account. Since we are going to be using anonymous accounts, we don't need to ask visitor for any information:

<button
  on:click={onRegister}
  class="flex items-center justify-center space-x-3 bg-brand-600 hover:bg-brand-500 text-white rounded-none px-10 py-3"
  >
    {#if registering}
      <span>...</span>
    {/if}
    <span>Create Anonymous Account</span>
</button>

All of this goes into src/routes/index.svelte.

Next let's add method that runs when we click the button, as well as registering variable indicating loading status:

<script lang="ts">
  let registering = false;
  async function onRegister() {
    if (registering) { return; }
    registering = true;

    try {
      await AppwriteService.createAccount();
      await goto('/casino');
    } catch (err: any) {
      alert(err.message ? err.message : err);
    } finally {
      registering = false;
    }
  }
</script>

Finally, let's add a logic to redirect user to /casino route if we can see he is already logged in. This will allow visitors getting to / to be automatically redirected to casino if they are already logged in:

<script context="module" lang="ts">
  import { browser } from '$app/env';
  import { goto } from '$app/navigation';
  import { Alert } from '$lib/alert';
  import { AppwriteService, type Profile } from '$lib/appwrite';
  import Loading from '$lib/Loading.svelte';

  export async function load(request: any) {
    try {
      const account = await AppwriteService.getAccount();
      const profile = await AppwriteService.getProfile(account.$id);

      return { status: 302, redirect: '/casino' };
    } catch (_err: any) { }

    return {};
  }
</script>

That concludes our authentication page ๐Ÿ˜… If you are feeling advantageous, feel free to add some cool styles around it.

๐ŸŽฒ Game Page

Let's make a file src/routes/casino.svelte to register our new route. In this file, let's start by writing a logic of loading the data. In there let's load user's account information, as well as profile:

<script context="module" lang="ts">
  import { browser } from '$app/env';
  import { goto } from '$app/navigation';
  import { Alert } from '$lib/alert';
  import { AppwriteService, type Profile } from '$lib/appwrite';
  import Loading from '$lib/Loading.svelte';

  export async function load(request: any) {
    try {
      const account = await AppwriteService.getAccount();
      const profile = await AppwriteService.getProfile(account.$id);

      return { props: { account, profile } };
    } catch (err: any) {
      console.error(err);

      if (browser) {
        return { status: 302, redirect: '/' };
      }
    }

    return {};
  }
</script>

By getting a profile, it is automatically created if not present already. That will automatically give us a balance of 500 almost dollars.

These information can now be received in a JavaScript variable:

<script lang="ts">
    import type { Models, RealtimeResponseEvent } from 'appwrite';

    // Data from module
    export let account: Models.User<any> | undefined;
    export let profile: Profile | undefined;
</script>

You can ignore types import for now. They will come into play later. Just make sure to keep it there ๐Ÿ˜›

Next, let's show our account information. For sick of simplicity, we will only show the ID of the account:

<p class="mb-8 text-lg text-brand-700">
  There we go, account created! Don't believe? This is your ID:
  <b class="font-bold">{account?.$id}</b>
</p>

Let's also prepare a button for user to sign out:

<button
  on:click={onSignOut}
  class="flex items-center justify-center space-x-3 border-brand-600 border-2 hover:border-brand-500 hover:text-brand-500 text-brand-600 rounded-none px-10 py-3"
  >
    {#if signingOut}
      <span>...</span>
    {/if}
    <span>Sign Out</span>
</button>

Next let's show user's fake dollars balance:

<h2 class="text-2xl font-bold text-brand-900 mb-8 mt-8">Balance</h2>
<p class="mb-3 text-lg text-brand-700">Your current blalance is:</p>
<p class="mb-8 text-3xl font-bold text-brand-900">
  {profile?.balance}
  <span class="text-brand-900 opacity-25">Almost Dollars</span>
</p>

Lastly, let's add section for betting:

<input
  bind:value={bet}
  class="bg-brand-50 p-4 border-2 border-brand-600 placeholder-brand-600 placeholder-opacity-50 text-brand-600"
  type="number"
  placeholder="Enter amount to bet"
/>

<button
  on:click={onBet('heads')}
  class="flex items-center justify-center space-x-3 border-2 border-brand-600 hover:border-brand-500 bg-brand-600 hover:bg-brand-500 text-white rounded-none px-10 py-3"
>
  {#if betting}
    <span>...</span>
  {/if}
  <span>Heads!</span>
</button>

<button
  on:click={onBet('tails')}
  class="flex items-center justify-center space-x-3 border-2 border-brand-600 hover:border-brand-500 bg-brand-600 hover:bg-brand-500 text-white rounded-none px-10 py-3"
>
  {#if betting}
    <span>...</span>
  {/if}
  <span>Tails!</span>
</button>

With all of that, HTML part of our casino page is ready! Let's start adding the logic by first adding method for signing out:

let signingOut = false;
async function onSignOut() {
  if (signingOut) { return; }
  signingOut = true;

  try {
    await AppwriteService.signOut();
    await goto('/');
  } catch (err: any) {
    alert(err.message ? err.message : err);
  } finally {
    signingOut = true;
  }
}

Next let's add a logic for our betting section, to allow submitting our coin flip bet:

let bet: number;
let betting = false;
function onBet(side: 'heads' | 'tails') {
  return async () => {
    if (!profile || !account || betting) { return; }
    betting = true;

    try {
      const didWin = await AppwriteService.bet(bet, side);
      if (didWin) {
        alert(`You won ${bet} Almost Dollars.`);
      } else {
        alert(`You lost ${bet} Almost Dollars.`);
      }
    } catch (err: any) {
      alert(err.message ? err.message : err);
    } finally {
      betting = false;
    }
  };
}

Let's finish off by adding a real-time subscription to our profile. This will automatically update the balance displayed on the website as soon as it changes on the backend:

$: {
  if (profile && browser) {
    AppwriteService.subscribeProfile(profile.$id, onProfileChange);
  }
}

function onProfileChange(response: RealtimeResponseEvent<Profile>) {
  profile = response.payload;
}

We have just finished the casino page! And.. I think... ๐Ÿค”

That makes our Almost Casino complete!!! Let's now look at the main topic of this application, ๐ŸŒŸ SSR ๐ŸŒŸ.

โšก Server Side Rendering

Let's do the magic! ๐Ÿง™

We start by creating src/hooks.ts file. In there, we intercept each request and get Appwrite authorization headers. We also return it in order to later retrieve it as part of our session object:

import * as cookie from 'cookie';

export function getSession(event: any) {
    const cookieStr = event.request.headers.get("cookie");
    const cookies = cookie.parse(cookieStr || '');
    const authCookie = cookies[`a_session_${import.meta.env.VITE_APPWRITE_PROJECT_ID.toLowerCase()}_legacy`];

    return {
        authCookie
    }
}

For this to work, make sure to install 'cookie' library with npm install cookie.

Next, at the beginning of every load() function (one in src/routes/index.svelte, one in src/routes/casino.svelte), let's add these two lines to extract our authentication headers from the session, and apply then to Appwrite SDK:

// Using SSR auth cookies (from hook.ts)
if (request.session.authCookie) {
  AppwriteService.setSSR(request.session.authCookie);
}

With this in place, SSR will now work properly! ๐Ÿ˜‡ You might not see it yet due to cookies following same-domain policy, but we will address that in the deployment section.

๐Ÿš€ Deployment

Since we will deploy to Vercel, let's make sure we use the correct adapter. We start by installing the adapter:

npm i @sveltejs/adapter-vercel

Next, let's update our svelte.config.js to use our new adapter:

import adapter from '@sveltejs/adapter-vercel'; // This was 'adapter-auto' previously, we just need to rename to 'adapter-vercel`

// ...

const config = {
  // ...
  kit: {
    adapter: adapter({
      edge: false,
      external: [],
      split: false
    })
  }
};

// ...

This makes our app ready for Vercel! ๐Ÿ˜Ž Let's make a Git repository out of our project, sign in to Vercel and deploy the app. There is no need for any special configuration.

Once the application is deployed, make sure to put both Appwrite and Vercel applications on the same domain in order for SSR to work with authentication cookies.

If your application runs on the root of the domain, for instance, mycasino.com, then your Appwrite could be on any subdomain like api.mycasino.com.

If you plan to host your application on a subdomain, make sure to host it under the subdomain on which Appwrite runs. For instance, you could have Appwrite on domain mycasino.myorg.com and Vercel application on domain app.mycasino.myorg.com.

๐Ÿ‘จโ€๐ŸŽ“ Conclusion

We successfully built a project that confirms Appwrite plays well with all technologies out there. I am really happy I got this article out as there were many requests from the Appwrite community regarding SSR, and now I have a place to point them to, even with code examples ๐Ÿ˜‡

This demo application can also serve as an example for building the same SSR logic with other frameworks such as Angular, Vue, or React. They all follow a really similar structure regarding SSR, and it all comes down to extracting cookies from requests and setting them on Appwrite SDK.

If you are bookmarking this article, here are some highly valuable links for you to keep an eye on:

ย