DEV Community

Cover image for Appwrite Hand-In-Hand with Svelte Kit (SSR)
Matej Bačo
Matej Bačo

Posted on • Updated on

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

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
Enter fullscreen mode Exit fullscreen mode
✔ 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
Enter fullscreen mode Exit fullscreen mode

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

$ cd almost-casino
$ npm install
$ npm run dev
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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;
    }
  };
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
    })
  }
};

// ...
Enter fullscreen mode Exit fullscreen mode

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.

🔗 Useful Links

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

Top comments (2)

Collapse
 
synchromatik profile image
synchromatik • Edited

When i go to Almost Casino: Live Demo and disable JS, button is not working. What is SSR part in that app?

Collapse
 
gds profile image
Mehmet Fatih Pazarbaşı

Hey thank you for this example.
Can you please update it to the latest SvelteKit 1.0 so that it's a good template to refer to for SSR folks?