If something is not clear or you run into problems please file an issue and I'll update the docs accordingly.
A quick overview of the frameworks and how they are used.
Passlock handles passkey registration and authentication.
Passlock also handles social sign in, abstracting passkey and google authentication users into a common "Principal". The abstraction allows this app to use the same code to handle both passkey and social authentication.
During registration and authentication, Passlock returns a token. We send this token to the backend actions and verify it's authenticity via the Passlock REST API.
Lucia handles sessions. Put simply, when a user authenticates, Passlock returns a token. A backend +page.server.ts
action verifies the token is authentic, then creates a Lucia session. Lucia supports many database backends, this example uses sqlite.
Superforms makes it really easy to handle forms. We use Superforms for both the registration and authentication forms. Passkey registration & authentication hooks into Superforms events (see below).
Melt UI is a headless component builder library for Svelte. It allows us to build accessible, interactive elements.
Preline is an awesome Tailwind UI library. Unfortunately whilst the Preline JavaScript can be used with Svelte, it's a bit clunky and doesn't play too well with Svelte.
However, Melt UI and Preline CSS classes are an awesome combination. We use Preline for styling and Melt UI for behavior.
Note
Unlike shadcn-svelte, I haven't ported the entire Preline framework across to Svelte. I've simply used Melt UI to build the components required by this app. However as you will see, it's really easy to build a component using Melt (or bits-ui)
The code is quite well commented so please check it out. I'll give a quite overview of the operations, with pointers to the relevant source code.
See src/routes/(other)/+page.svelte. The basic approach is to use Superforms events:
- User completes and submits a form.
- Superforms intercepts the submission
- We ask the Passlock library to create a passkey
- This returns a token, representing the new passkey
- We attach this token to the form and submit it
- In the src/routes/(other)/+page.server.ts action we verify the token by exchanging it for a Principal
- This principal includes a user id
- We create a new local user and link the user id
See src/routes/(other)/login/+page.svelte. Very similar to the registration:
- User completes and submits a form.
- Superforms intercepts the submission
- We ask the Passlock library to authenticate using a passkey
- This returns a token, representing the passkey authentication
- Attach this token to the form and submit it
- In the src/routes/(other)/login/+page.server.ts action we verify the token by exchanging it for a Principal
- This principal includes a user id
- Lookup the local user by id and create a new Lucia session
Similar conceptually, the heaby lifting is offloaded to Passlock. We just need to deal with the token (and some UX stuff)
- User clicks the Apple/Google button (or one tap)
- Apple/Google authenticates the user
- We ask the Passlock library to process their response
- This call returns a token, representing the user
- As for passkey registration/authentication
We want to protect the /app
route. We do this in src/hooks.server.ts by checking the route id and user/session status.
During the registerPasskey()
call you can pass a verifyEmail
option:
// email a code
const verifyEmail = {
method: 'code'
}
// email a link
const verifyEmail = {
method: 'link',
redirectUrl: 'http://localhost:5174/verify-email/link'
}
// verifyEmail can also be undefined in which case
// no verification mails will be sent
passlock.registerPasskey({ verifyEmail })
Passlock will generate a secure code or link and email it to the user during the passkey registration. The src/routes/(other)/+page.server.ts action then redirects the user to one of two pages:
- /verify-email/awaiting-link - Prompts the user to check their emails
- /verify-email/code - Prompts the user to check their emails and enter the code
If the verifyEmail
method is link
you must also provide a url to which the user will be sent when they click the link. You should use the route /verify-email/link
.
Passlock will send the user to this route, appending a ?code=xxx
query parameter. In the src/routes/(other)/verify-email/link/+page.server.ts load function we grab this code and feed it into src/routes/(other)/verify-email/link/+page.svelte. This page presents the user with a button. When the button is clicked we call Passlock to verify the code is authentic.
Tip
Why do it this way? Why not simply verify the code in the +page.server.ts load function? Because we may need to re-authenticate the user. For background please see the passlock docs