How to Exchange Data Between Client and Server Using SvelteKitAuth

Chapter 12 of Comprehensive Guide to SvelteKitAuth: Secure Authentication for SvelteKit Apps

In this article, we will explore how to exchange data between the client and server using SvelteKitAuth. We will delve into the importance of JWT and session callbacks, and how to synchronize and store sessions effectively.

Updating and Syncing data from the Client to the Server

Since the SvelteKitAuthConfig is attached to the hooks, it triggers with every API call that is intercepted by the hooks. The SvelteKitAuthConfig comes with two important callback functions, i.e., jwt and session, that trigger on every network request intercepted by the hooks.

The jwt callback holds the token property, which includes the authenticated user's token and profile data. This information constitutes the session information that will be shared site-wide. This information is stored in an encrypted format in the session-token cookie.

On every network request, this callback decrypts the session-token cookie, reads the information, performs operations, and then encrypts it back and saves it in the session-token cookie. This token is then passed to the session callback, which forms the session object that will be shared site-wide and can be accessed on the server as const session = await locals.auth() and on the client-side as const session = $page.data.session.

Hence, in order to update the user information, we must use the jwt callback so that it is preserved in the session-token cookie and later pass the information to the session callback so that the information is accessible on both the server and the client side.

The First Step: Updating the Config Object

With this knowledge in hand, let's configure the jwt and the session callback in the SvelteKitAuthConfig object.

{
  callbacks: {
    async jwt({ token, account, profile }) {
      ...
      const userQuery = event.request.headers.get('query') || event.url.searchParams.get('query');
      if (userQuery === 'update-user-data') {
        try {
          const clonedRequest = event.request.clone();
          const clonedBody = clonedRequest.body;
          const response = await new Response(clonedBody);
          const body = await response.json();
          if (!isEmpty(body)) {
            token = {
              ...token,
              ...body
            };
          }
        } catch (ex: any) {
          console.log('Unable to update user data for url', ex?.message);
        }
      }
      return token;
    },
    async session({ session, token }) {
      if (session.user) {
        if (token?.access_token) {
          session.user = { ...session.user, ...token } as any;
        }
      }
      return session;
    }
  }
}

Let's take a moment to understand what the above code snippet actually does:

  • Since the jwt callback is invoked for every API request intercepted by the hooks, we cherry-pick the ones that have a particular flag passed via the header or the query params, which will instruct us that we must perform data communication operations. For the sake of this example, we use the string update-user-data.

  • Once we get hold of this flag, we will fetch the payload from the request and update the token property of the jwt callback. Remember, this is the same property that will be preserved in an encrypted format in the session-cookie. Side Note: cookie size up to 4kb could be saved at any given time due to the browser's storage limitation. However, if we pass too much data into the token property, they will be chunked into multiple cookies, i.e., session-cookie_0, session-cookie_1, and so on.

  • Finally, we pass the token property to the session callback so that it is available for use on both the client and the server side.

The Second Step: Creating the Update Endpoint

As we discussed in the last section, we need to intercept specific API requests that have flags which will enable us to kickstart the data operation. So in this section, we will create an API route that will be used by the client to send the payload which must be synced and updated on the server side.

So we create an API route in the file src/routes/api/update-user-data/+server.ts. The aim of this API route is to pass data to the jwt callback. The API route then sends the updated data back, which we can catch by invoking the auth() method. We then return the updated object back to the client.

import type { RequestHandler } from '@sveltejs/kit';
import { isEmpty } from 'lodash-es';

export const POST = (async ({ locals }) => {
  const session = await locals.auth();
  const user = session?.user;

  if (!isEmpty(user) && !isEmpty(session)) {
    try {
      return new Response(JSON.stringify({
        data: user
      }), { status: 200 });
    } catch (error: any) {
      console.log('Error while updating user data: ', error?.message, '. Sending existing data');
      return new Response(JSON.stringify({ data: user }), { status: 200 });
    }
  } else {
    return new Response(JSON.stringify({ data: 'user is not authorized to update data' }), { status: 401 });
  }
}) satisfies RequestHandler;

The Third Step: Client Initiates the Update Request

The client triggers the API route that we created in the previous section and sends the payload object along with it.

<script lang="ts">
  let userData = $page.data.session?.user?.fav_num || '';

  async function updateUserData() {
    const request = await fetch('/api/update-user-data?query=update-user-data', {
      method: 'POST',
      body: JSON.stringify({
        fav_num: `My favourite number is: ${Math.ceil(Math.random() * 100)}`
      })
    });
    const response = await request.json();
    userData = response?.data?.fav_num;
  }
</script>

<button on:click={updateUserData} class="button">Update user data</button>

{#if userData}
  <p>Updated user-data {userData}</p>
{/if}

With these three steps, the data is synced and updated by the client to the server. In the next section, we will learn how to update and sync the data from the server to the client.

Updating and Syncing Data from the Server to the Client

The only way for the server to send the information back to the client is via hydration. The server load functions, i.e., +layout.server.ts and +page.server.ts, can send data to the client by returning an object. Only objects that can be serialized using devalue can be used. Here is an example where the server load function returns the session property to the client.

NOTE: Properties returned by the layout server load function will be available to all the child routes, whereas the properties returned by the page server load will only be available to the current route. If both functions return the same property, the one that returns the last will precede over others.

// server load functions

import type { LayoutServerLoad } from './$types';

export const load: LayoutServerLoad = async (event) => {
    const session = await event.locals.auth();
    return { session };
};

The client can access this information using the $page.data property.

<script lang="ts">
  import { page } from '$app/stores';

  let session = $page.data.session;
  let user = $page.data.session.user;
</script>

Conclusion

In this article, we explored the process of exchanging data between the client and server using SvelteKitAuth. We delved into the importance of JWT and session callbacks, and how to effectively synchronize and store sessions. By configuring the jwt and session callbacks, creating an update endpoint, and initiating update requests from the client, we ensured seamless data synchronization from the client to the server. Additionally, we discussed how the server can send information back to the client through hydration using server load functions. By following these steps, you can maintain a consistent and secure data exchange between the client and server in your SvelteKit applications.

Here is the link to the GitHub repository with the codebase. In the next article, we will learn to how to build custom pages and handle events in SvelteKitAuth.

Did you find this article valuable?

Support Aakash Goplani by becoming a sponsor. Any amount is appreciated!