A SaaS starter boilerplate with elegant UX to use as reference or point to build upon.
The focus here was to create a serverless app that utilizes modern tooling, infrastructure, and is centralized to as few platforms/expertise as possible.
There are a few remaining baseline items you should considering adding, but hopefully this provides a solid head start if you're choosing to work with these tools or platforms.
- SvelteKit
- Netlify Identity authentication / GoTrue
- Stripe subscriptions & customer portal
- Fauna GraphQL database
- Tailwind
- SveltePreprocess for Pug markup
- Deployment as serverless app on Netlify via SvelteKit's Netlify Adapter
- Authentication - via JWT cookie & Netlify Identity / GoTrue
- Authentication email templates
- Authentication & account management UX
- Helper methods to access GoTrue API without JS client
- Subscription billing and management - via Stripe
- User database - via Fauna
- Helper methods to access Fauna GraphQL API without JS client
- Responsive layouts
- Configurable color system - through Tailwind
- Configurable form components with flexible, browser-based validation via Constraint validation API
- Configurable modal component
- Configurable notifications component
- Combining of SvelteKit serverless functions (aka endpoints) with custom Netlify functions
- Demo Site
- Kick the tires…
- sign up and confirm as a new user
- login / logout
- retrieve lost password
- update email address
- update password
- manage subscription & billing method
- delete account
There are a few areas I didn't had time to tie up in this repo, but would recommend looking into –
- add measures to sanitize user-entered data at endpoints (example)
- add measures to secure JWT cookie against CSRF attacks (details; look for modern simpler fixes with JWT cookies & serverless architectures)
- ensure HTTP security headers are set properly to prevent common security issues
- add Terms of Service content & agreement (checkbox) at sign-up and save election to Fauna database when creating user
- to improve UX, add messaging when a user's session expires (see
sessionExpired
flags in code for start of this) - to improve UX, add functionality to refresh user's JWT cookie before expiration during an active session (see
touchTime
andauthExpires
in code, and methods to get and (re)save refresh token in Fauna for start of this) - add views and UX to handle attempted logins from accounts with cancelled or paused subscriptions
- implement UX/UI testing library at this point, before app grows further (e.g. Cypress)
- implement a more robust logging & monitoring solution (beyond current server console.logs), before app grows further
- test app's current accessibility, GDPR compliance, and performance
- configure Netlify Build Plugins to help automate and regulate any requirements you care about
- test Netlify Analytics with application to see if it's worth utilizing
The current Netlify Adapter has 2 primary limitations:
- Endpoints do not have access to
context.clientContext
data. This contains information needed to interact with services like Netlify Identity for admin actions. Details - Netlify triggers some functionality per specifically named functions (e.g. post user sign-up). Endpoint 'functions' are currently aggregated into a single
render
function, preventing specifically named endpoints/functions from being available post build. Details
Current Workaround
- For functionality that requires
context.clientContext
data, create a separate, custom serverless function that is copied post-build† into the finalfunctions
directory:- Example 1:
/src/additional_functions/delete-identity.js
(a function called explicitly by SK endpoints) - Example 2:
/src/additional_functions/handle-subscription-change.js
(a webhook triggered by an external event [Stripe subscription update])
- Example 1:
- For specifically named files that are triggered by events, also create a separate custom serverless function that is copied post-build† into the final
functions
directory:- Example:
/src/additional_functions/identity-signup.js
(function called automatically when a new user completes sign-up process)
- Example:
† The Netlify Adapter currently aggregate all endpoints (i.e. individual SvelteKit serverless functions) into a single serverless function called render
within your target 'functions' directory for Netlify. To add additional functions to this directory, package.json is configured to run a post-build script (that utilizes the cpy-cli
package) to copy items and npm-run-all
to execute multiple scripts when the 'build' script is called locally or on deploy to Netlify.
There's a lot of outdated and inaccurate information online when it comes to how to setup authentication correctly and securely. This can get even more confusing when you're trying to implement authentication in a static or serverless site.
The long and short of it is -
- use or build a system that creates relatively short authenticated sessions (e.g. 60 minutes)
- don't store or pass credentials in a place Javascript is able to access
- treat the system as stateless, passing and confirming authentication as needed
- leverage and manage SvelteKit's client state (session) thoughtfully
In this implementation that amounts to –
- using Netlify Identity to create / authenticate users and provision credentials in a JWT
- storing JWT claims as a httpOnly secure cookie that is set to last for the current session and maximum of 60 minutes before expiring. Currently, this requires the user to login again if session lasts longer than 60 minutes. As noted above, to improve UX, consider finishing the work to refresh the JWT cookie in the background for active sessions, using and resetting the refresh token saved for the user in Fauna at login.
- utilizing SvelteKit's hooks to access current access token from JWT cookie that is automatically passed by the browser on requests to routes and endpoints.
- within hooks, storing only non-sensitive data in the session that is passed and stored in the client state (e.g. email address shown in application header).
- gating data loaded / access to endpoints by validating access token passed from hooks on
request.locals
(example) - within _layout files
load
redirecting authorized / unauthorized visitors as needed (example); since this is done in client code it may be possible to circumvent (not certain if that applies to load functions or not), but at most they would see empty shells for authorized routes.
This implementation utilizes Netlify's authentication service, Identity (built on their GoTrue library) to help limit the number of platforms in use and also leverage the benefits of using sibling services on the same platform.
Identity offers a couple benefits in how it can interact with serverless functions that are also deployed on Netlify, but this could be replaced with a different authentication service if wanted.
At the time of creating this, I wasn't able to get Identity's GoTrue Javascript library to play nice with SvelteKit / serverless since it had dependencies that required access to window
.
I created custom helper methods to access the same underlying GoTrue functionality in auth-api-methods.js. Most of these directly access the API endpoints for your Identity instance. A couple require admin privilege (e.g. deleting a user). This is provided by calling a custom function (within 'additional_functions) where Netlify grants an Identity admin token to access the final endpoint and complete the request.
In this project, I wanted to leverage the functionality of each platform to it's fullest before reaching for additional tooling / infrastructure, where possible.
For form inputs & client-side validation the platform of the browser itself offers a great deal of ability and control - via HTML5 inputs and the browser's Constraint validation API.
Within the form components set, there are several core components that mirror their HTML counterparts: Input, Textarea, Select, Button. Each of these includes a broad range of configuration options for their functionality and styling.
Additionally, there are several pre-built configurations of the core components for common, special uses: CheckboxGroup, RadioGroup, InputPassword, InputUrl, PrefixedInput.
Each of these inputs can implement standard and custom validation rules. 'Standard' validation is provided by the input's semantics (e.g 'type=email'). 'Custom' validation can be prescribed by adding custom validation regex (example) and/or prescribing a set of custom errors and/or warnings (example).
These hooks for prescribing validation rules also allow easily creating and enforcing validation across multiple inputs (example).
Lastly, the configurable Form component will by default intelligently check whether it's inputs are valid or not and updates it's action buttons accordingly.
The form, modal, and notifications components demo'd and included in this repo should be easily transferable to other projects. Copying and pasting these as discrete files, rather than installing as a package, allows you to tinker with and adjust them as needed for your specific needs.
The components have some interdependencies currently in this repo (e.g the modal component includes some form components), but these can be removed if wanted.
Inspect the components' exported variables and comments to see what configuration options are available and search for instances of the component across this code base to see additional examples of their use.
Tailwind has a robust range of colors available out of the box. In addition to these, it's helpful to also be able to use semantic colors (e.g. for actions, brand related content) and also still leverage Tailwind's utility classes.
This is done via a helper function (cssVarHslHelper
) and addition of color objects (brand
, action
) in tailwind.config.cjs.
These values these new colors are then defined via CSS variables in app.css.
Once defined they cane be used the same way you would use Tailwind's built in colors: e.g. bg-indigo-500
can be replaced with bg-brand-dark
, etc. (Note that the colors variants defined with as DEFAULT
in tailwind.config.js do can be called with the base name alone; e.g. bg-action
).
At time of publishing, current versions in use are -
- @sveltejs/kit 1.0.0-next.115
- @sveltejs/adapter-netlify 1.0.0-next.17
If things don't work or project cannot build successfully, either
- A) double-check there have not been any breaking changes in SvelteKit changelog or Netlify Adapter changelog, or
- B) update package.json to use the versions above, and work forward from there.
To implement a fully working copy of this repo, follow the steps below –
-
Create accounts if needed for -
- Netlify.com
- Fauna.com
- Stripe.com
- Note: can use free plans
-
Install & authenticate Netlify CLI
-
Clone repo locally & publish copy to own GitHub account
-
Within terminal & directory of local copy of repo:
$ ntl init
- select 'Create & configure new site' & desired Netlify team
- choose site name
- confirm build defaults ('npm run build', 'build' & 'functions' directories)
- Note: Netlify will deploy a version of site at site name URL, but this is not fully functional yet
-
Enable & Configure Netlify Identity
$ ntl open:admin
- click on 'Identity' link in header and select 'Enable Identity'
- click on 'Settings' link in header and 'Identity : Email' links in sidebar
- edit the path settings for email templates:
- Invitation:
/auth-email-templates/invited.html
- Confirmation:
/auth-email-templates/confirm.html
- Recovery:
/auth-email-templates/password-recovery.html
- Email Change:
/auth-email-templates/email-change.html
- Invitation:
- can also update Subject lines for each template, as desired
-
Install and authenticate Netlify Fauna DB addon
$ ntl addons:create fauna
$ ntl addons:auth fauna
- this will open Fauna and ask you to import DB created via Netlify add-on
- you can name DB anything you'd like (e.g. same name as site name)
- Note: this Netlify Fauna add-on will also create the API keys needed as env vars that are not exposed in Netlify's UI but are visible when you run
$ ntl env:list
-
Configure Fauna database
- post import, select 'Create New Collection', name 'User' and save
- select 'GraphQL' link in sidebar and select 'Import Schema'
- import
/src/lib/apis/db-api-schema.gql
- under 'Collections' link in sidebar should see new, empty 'User' collection
-
Configure Stripe products
- ensure account is in test mode with toggle setting in sidebar
- add sample recurring products & prices (i.e. plans) (example tutorial)
- Note: in the current code, the first word the 'Price description' value for each price for each product will be used as the user's role in Netlify Identity. This is available under the 'Additional options' shown for each price.
-
Configure Stripe Customer Portal
- go to https://dashboard.stripe.com/test/settings/billing/portal
- within 'Functionality' section:
- turn on Payment methods: 'Allow customers to update their payment methods'
- turn on Update subscriptions: 'Allow customers to switch to different pricing plan'
- within 'Products' section:
- add all test products created under 'Added Products'
- within 'Business Information' section:
- enter a value for 'Terms of Service' (e.g. https://www.domain.tld/tos)
- enter a value for 'Privacy' (e.g. https://www.domain.tld/privacy)
- click 'Save'
-
Create Stripe subscription update webhook
- select 'Developers' : 'Webhooks' link in sidebar and 'Add Endpoint' button
- for 'Endpoint URL' enter: https://<YOUR_SITE_NAME>.netlify.app/.netlify/functions/handle-subscription-change
- in 'Events to send', search for and select
customer.subscription.updated
- save endpoint
-
Add Stripe keys as environment variables
- select default product/price to apply to new customers (e.g. Free Plan) and copy 'API ID' value
- save value as Netlify env var:
$ ntl env:set STRIPE_DEFAULT_PRICE_PLAN your-product-API-ID
- save value as Netlify env var:
- select 'Developers' : 'API keys' link in sidebar and copy 'Secret key' value
- save value as Netlify env var:
$ ntl env:set STRIPE_SECRET_KEY your-stripe-secret-key
- save value as Netlify env var:
- select 'Developers' : 'Webhooks' link in sidebar, select earlier created webhook and copy 'Signing Secret' value
- save value as Netlify env var:
$ ntl env:set STRIPE_UPDATES_WEBHOOK_SECRET your-webhook-signing-secret
- save value as Netlify env var:
- if you run
$ ntl env:list
you should now see 6 variables for:- STRIPE_DEFAULT_PRICE_PLAN
- STRIPE_SECRET_KEY
- STRIPE_UPDATES_WEBHOOK_SECRET
- FAUNADB_ADMIN_SECRET
- FAUNADB_SERVER_SECRET
- FAUNADB_CLIENT_SECRET
- select default product/price to apply to new customers (e.g. Free Plan) and copy 'API ID' value
-
Rebuild / deploy site
- Note: site needs to be rebuilt with new env variables in place to be functional
- within Netlify admin, select 'Deploys' link in header
- within 'Trigger Deploy' menu select 'Clear cache and deploy site'
-
Create and test new user
- on site's welcome page, sign-up a new user (via 'start your 14-day free trial' link)
- at this point you will see
- a new user in Netlify : Identity admin without any role assigned (this user is created but not confirmed)
- when you sign-up you should receive a sign-up confirmation email
- email link should open site and confirm your account
- at this point you should see
- a role (e.g. 'free') assigned to user in Netlify : Identity admin
- an entry in Fauna DB that contains this user's Netlify ID, Stripe ID, and current refresh token
- a new customer created in Stripe with user's email address with the default plan assigned (e.g. Free)
- from /account can also
- update email address (with associated confirmation email)
- update password
- manage subscription plan via Stripe customer portal
- delete account
- signing in/out
- sets and removes a JWT httpOnly cookie for authentication
- sets and removes a refresh token for user in Fauna database
- updating subscription plan via /account 'manage' link
- updates user's subscription in Stripe customer account
- triggers webhook function to update user's Netlify Identity role to first word or product/plan name
- to test can use card info '4242 4242 4242 4242' 12/30 424 90210
-
To run site locally
- install dependencies:
$ npm i
- run SvelteKit with Netlify dev:
$ ntl dev
- notes:
- most actions are available locally but some will create temporary false errors. For example, when logging in a user that exists in Netlify Identity you may see an 'Unable to Process' message and a 405 network response. This may be an issue with Netlify Identity & Netlify Dev. Repeat the action 1-2 times and it will proceed normally.
- to login in as user locally, user must exist in Netlify Identity (i.e. have signed up via locally run site or production site)
- confirmation emails will link to the production site
- install dependencies: