SvelteKit + Oauth2 + Lucia + Postgres + Threejs

7.12.2023


Github repo for this project: SvelteKit-Oauth2Lucia-Postgresql-Threejs

Account/User management is a common need in web applications. The ability to quickly create an account can make a big difference in the user experience. Having a secure authorization and authentication process can also be vitally important, depending on the type of information being stored. In any case it's a good idea to use the most secure practices available. There are a lot of different ways to handle user creation and authorization. This example was trying to follow these goals:

  • to quickly and seamlessly handle account creation and management
  • to be as secure as possible or necessary
  • to be as efficient with resources and cost as possible


The initial reason to develop the example was to support a feature for a web based game where players have in game data persisted. In the end I chose to go a different route and use localstorage for the game to store data on the client side. Either solution could have worked, the main benefit of localstorage is that it's totally free, whereas Vercel Postgres has a monthly limit on compute time before charging based on usage. However I was interested to try the new Storage offering from Vercel and test the beta Vercel Postgres database. This means the project also has to work with a Vercel Postgres database and would be able to update the persistent data store from within the game which is a ThreeJS / Threlte project.


Oauth2 is a great solution for initial authentication and account creation. This allows a very fast way of creating an account without needing to enter a new account name or password and also completely removes the necessity of storing passwords. Of course there may be situations where you need to support those options, but if Oauth works then you don't have to worry about cyrptography and storing sensitive user data.


Lucia is an open source authentication and user management library. This will help with some of the basic user functions like creating an account and checking if one exists. It also has integrations with OAuth providers which is how I wanted to do the sign up process. It supports lots of databases and web frameworks like 'SvelteKit, Next.js, Express, and Astro'.


Extra Requirements

  • In addition to setting up the files this requires the Vercel CLI and a few commands to link the project to the storage DB. You can also set up and see the connection in the Vercel dashboard.
  • since we are using Oauth2 we will use a Github oauth2 app (could be another oauth provider or multiple providers)

User flow looks like this:

  • When a user is detected to not be logged in, Redirect user to the signup/login page
  • User is presented option to sign in with github
  • User is asked to give permission to our oauth app
  • Return to application with a github access token
  • Use token to make a call to github api
  • Check if the github account is associated with an account in our DB. If it is we can return that users info, If not we can create a new account
  • Now when a user is logged in they can access other parts of the app, they can sign out, and session cookies are handled by Lucia


Now that an account has been created when the user returns and is logged in we can access their database entry on the server and pass what we want to the client. For this example I wanted to store an object associated with each user that would store game data such as a high score. Now the personal high score needs to be read into the game and be able to be updated from within the game.


With the PageServerLoad we can return different values from the database in page.server.ts, then access them in the +page.svelte as a variable. This allows you to use the returned data on the page or pass it to a component. The example does both. It displays the postgres tables on the page as well as passes the data to the Scene component as 'authData'.

import type { PageData } from './$types';
export let data: PageData;

console.log('from routes/page.svelte', data.auth_user)

<Canvas>
    <Scene authData={data.auth_user} />
</Canvas>

Now within the Scene component which is a ThreeJS scene we have the user's DB object. With that working there needs to be a way to make a new db call from within a threeJS Scene, a way to update the user object in the DB after some event in the game. One way to do this is to use a server file to add an api endpoint as suggested here in the sveltekit docs: SvelteKit Form Action Alternatives

Form actions are the preferred way to send data to the server, since they can be progressively enhanced
but you can also use +server.js files to expose (for example) a JSON API. Here's how such an 
interaction could look like:

send-message/+page.svelte
==========================
<script>
    function rerun() {
        fetch('/api/ci', {
            method: 'POST'
        });
    }
</script>

<button on:click={rerun}>Rerun CI</button>

api/ci/+server.js
=========================
/** @type {import('./$types').RequestHandler} */
export function POST() {
    // do something
}

The file /api/ci/+server.ts is a similar format to the example from the Svelte docs. It takes a users ID from the current session and will update the corresponsding user in the database. The contents being updated are just the game_stats object. That object is being passed from the Scene. If you want you can access and alter the game_stats object in the api server file before updating the database.


/api/ci/+server.ts
=============
/** @type {import('./$types').RequestHandler} */
export async function POST({ locals, request }) {

  // get the data parameters from the request
  const data = await request.json();
  data.highScore+=128
  let dataJson = JSON.stringify(data)

  // get the user id from auth
  const { user } = await locals.auth.validateUser();
  let userCookie = locals.auth.context.request.headers.cookie
  userCookie =  userCookie.split(';').filter(x => x.includes('auth_session'))
  let auth_sessionId = userCookie[0].split('=')
  let testvalidateSessionUser = await auth.validateSessionUser(auth_sessionId[1]); 
  let userId = testvalidateSessionUser.user.userId

  await db.query(`UPDATE auth_user
  SET game_stats = '${dataJson}'
  WHERE id = '${userId}';`);

  return new Response(user, {
      status: 200,
  });
}

Then from within our Scene JS script you can make a POST request to the new endpoint updated with some attributes from our Scene component:


Scene.svelte
=============
//remote DB function
function dbRemote() {
        fetch('/api/ci', {
            method: 'POST',
            mode: 'cors',
            body: JSON.stringify({
              "levelsComplete": 10,
              "highScore": 100,
              "completed": false
            }),
            headers: {
              "Content-type": "application/json; charset=UTF-8"
            }
  });
}


Now all the functions we were looking for are available with simple account creation, persistent storage, session management, and the ability to retrieve and update data from convenient endpoints.