Aether
Aether is a boilerplate for the RedwoodJS framework. It provides an additional set of opinionations to the framework, aiming to further decrease the time-to-market for full-stack web applications. These include various integrations, patterns, and architectures:
- Authentication (via dbAuth)
- Account confirmation
- Password reset
- Inviting members
- Multi-tenancy (i.e. Teams)
- RBAC
- Emailing (via
email-templates
) - Error and performance tracking (via Sentry)
- Local services — Postgres, Redis, ... (via Docker Compose)
- PR quality assurances (via
project-ci-action
) - Subscriptions and payment-processing (via Stripe)
- UI Design (via Chakra UI)
Installation
0) Prerequisites
- Ensure your system fulfills the RedwoodJS prerequisites.
- The Stripe CLI
- A Postgre database
- If you plan to use the provided
docker-compose.yml
— ensure you have installed Docker (Desktop) - See the RedwoodJS documentation for a local Postgres setup.
- If you plan to use the provided
1) Copy the repository
You will need a copy of the Aether repository in order to start making modifications. The recommendation is to clone it to your local machine.
git clone https://github.com/LockTech/aether.git
2) Reset git history
Delete the .git
directory, removing the Aether repository's history and reference.
rm -rf .git
Once deleted, you can initalize a new repository.
git init
3) Install dependencies
yarn install
4) Remove Aether's license
Aether is available under the MIT license — you are under zero obligation to use the same license for your project, and are encouraged to delete Aether's before starting.
rm LICENSE
Configuration
See Aether's .env.defaults
for a list of the environment variables which the boilerplate supports for configuration.
During development (
yarn rw dev
), only variables including the character-sequence...
are required.
Local services
Aether includes a Docker Compose configuration for starting a PostgreSQL server, powered by Supabase's fork. By default, this database should be configured using the POSTGRES_*
environment variables — added to .env.defaults
.
Adding Services
Consider your application requires a Redis database to provides its functionality. While developing your application, you can easily start-and-stop this (and your Postgres) database using a single command by adding a service
definition:
This example uses the Bitnami Redis image, as it supports configuration via environment variables.
services:
postgres:
...
redis:
image: bitnami/redis:7.0.4
env_file:
- .env.defaults
ports:
- 6379:6379
Then, in .env.defaults
(so it's available to all members of your team), add a password for your Redis database.
REDIS_PASSWORD=secret
Authentication
Aether is setup to use RedwoodJS' dbAuth.
Sign up
Signing up for an account with an Aether application should be done by a member of an organization (a.k.a. team, group, community, ...) who should have elevated access over the organization's resources — an administrator.
After creating their account, the administrator will be required to provide information about the organization: creating a Stripe customer as well as the user's organization. After being created, payment information will be collected from the admin — used to charge the organization for their subscription to the application.
Account Confirmation
Out of the box, Aether supports confirmation by-way of sending an email to the provided address at the time of signup or invitation. The email will contain a unique code: used by the application to assert the message was opened by the addresses' owner.
Password Reset
When a user attempts to reset their account's password, a message will be sent to their account's email containing a unique token used to assert the addresses' owner responded to the reset.
Inviting Members
The APIs are in place to allow an administrator to to invite other members of their organization to the application — reusing the same flow which allowed for the administrator to signup originally.
Multi-tenancy
Aether has been designed with a multi-tenant (a.k.a Team) architecture in mind. For this boilerplate, this means each user is expected to belong to one (and only one) organization
.
Current Organization
From within your application's services you will have access to the id
of the organization belonging to the current user via the currentUser.organizationId
property, found on the API context. As expected, this property will only be available from within services which require authentication.
Model Patterns
Prisma patterns which help to facilitate a multi-tenant architecture.
Uniqueness
When retrieving records from your database, you'll likely only want to find unique models based on some provided information — as well as the organization which the currentUser
belongs to. To facilitate this, it's recommended you define a compound unique constraint which includes the provided information as well as the field used to store the organization's id
.
Cascading Deletion
When an organization is deleted, it'll likely be desirable to delete all of the organization's related-records. This can be achieved by configuring a cascading delete when defining the foreign key relationship between the organization model and others.
Putting It Together
When combined, the uniqueness and cascading deletion topics described above should result in an implementation which resembles the following.
model Organization {
id String @id @default(uuid())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
somethings Something[]
}
model Something {
id String @id @default(uuid())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
organizationId String
@@unique([id, organizationId], name: "tenant")
}
Authorization
Aether makes use of RedwoodJS' @requireAuth
directive to facilitate authorizing access to an application's services. The directive expects three arguments, with two having defaults values which a majority of implementations are expected to make use of.
Name | Description | Default |
---|---|---|
organization | Assert the user either does or does not have an organization. | true |
roles | Check whether or not a user has the given role(s). | - |
subscribed | Assert the user either does or does not have an active subscription. | true |
Chakra UI
Aether's user interface has been implemented using Chakra UI.
Form Components
Provided in the web/src/components/Forms
directory are components which combine RedwoodJS' form components with Chakra UI's.
As of writing, this integration is incomplete, and should eventually be extracted out into a library. Progress on this can be tracked via the Aether repository.
Sending Emails
Aether makes use of the email-templates
package to send emails, rendering them using pug.
Sending In Development
When developing your application, emails will not be sent to the provided address. Instead, they will be opened in your default browser — alleviating the need to purchase or signup for a subscription in the early stages of product-development.
Typing Template Locals
The api/types/email.d.ts
file allows you to define the shape of your template's local
— the object which gets passed to the template.
See
api/src/lib/email.ts
to see how these types are put to use.
Sentry
Sentry is used for error and performance tracking. It has been configured for both the API and web sides — where it's expected both sides belong to a single project.
DSN
The SENTRY_DSN
environment variable should be used to provide a Sentry DSN — used to inform Sentry of where to send events for error and performance tracking.
Stripe
The Stripe platform provides the Aether boilerplate payment processing as well as subscription management functionality.
Price
In order to create a subscription, you will need to provide a price using the STRIPE_SUBSCRIPTION_PRICE
variable.
Webhooks
Two webhooks should be configured using Stripe, pointing to the following endpoints and triggered by the corresponding events:
/stripeSetupIntent
setup_intent.succeeded
/stripeSubscription
customer.subscription.created
customer.subscription.deleted
customer.subscription.updated
Invocations of these functions will be used to start and update an organization's subscription. The STRIPE_SETUP_INTENT_SECRET
and STRIPE_SUBSCRIPTION_SECRET
variables will be used to verify the origin of the webhook's signature — used to authenticate requests were sent by Stripe.
Introduction to Testing
RedwoodJS provides a variety of tools to facilitate testing an application. These tools are best considered as a new best friend.
Testing in Isolation
One of the prominent features of these tools is the ability to test your application in isolation. It's trivial to test individual components, services, or even lines of code — and this ability should be leveraged where possible. In practice, this often means developing the API and web-sides independently using tests (yarn rw test [api]
) and visual confirmation (i.e. Storybook) to assert expected results.
Back-end
For the API, Aether provides tests for all custom implementations — making use of RedwoodJS' directive, function, and service testing features.
Front-end
On the web, Aether focuses on making use of Storybook to develop and test the UI. Instead of relying on data existing in the database at a particular point in time, GraphQL operations can be mocked — and API-tests used to assert these mocked values align with its implementation.
PR Quality Assurance
Provided is a GitHub Action making use of RedwoodJS' project-ci-action
— which will build, lint, diagnos, and test the application whenever a PR is opened or updated.
VSCode: Explorer File Nesting
VSCode (version 1.67
and later) supports nesting files, allowing you to drastically decrease the amount of configuration files cluttering your editor. The following should be appended to .vscode/settings.json
.
{
"explorer.fileNesting.enabled": true,
"explorer.fileNesting.expand": false,
"explorer.fileNesting.patterns": {
".env.defaults": "*.env, .env.*, env.d.ts",
".gitignore": ".gitattributes, .gitmodules, .gitmessage, .mailmap, .git-blame*",
"index.d.ts": "*.d.ts",
"package.json": ".browserslist*, .circleci*, .codecov, .commitlint*, .editorconfig, .eslint*, .firebase*, .flowconfig, .github*, .gitlab*, .gitpod*, .huskyrc*, .jslint*, .lintstagedrc*, .markdownlint*, .mocha*, .node-version, .nodemon*, .npm*, .nvmrc, .pm2*, .pnp.*, .pnpm*, .prettier*, .releaserc*, .sentry*, .stackblitz*, .styleci*, .stylelint*, .tazerc*, .textlint*, .tool-versions, .travis*, .vscode*, .watchman*, .xo-config*, .yamllint*, .yarnrc*, api-extractor.json, apollo.config.*, appveyor*, ava.config.*, azure-pipelines*, bower.json, build.config.*, commitlint*, crowdin*, cypress.json, dangerfile*, docker-compose.yml, dprint.json, firebase.json, grunt*, gulp*, jasmine.*, jenkins*, jest.config.*, jsconfig.*, karma*, lerna*, lint-staged*, nest-cli.*, netlify*, nodemon*, nx.*, package-lock.json, playwright.config.*, pm2.*, pnpm*, prettier*, pullapprove*, puppeteer.config.*, renovate*, rollup.config.*, stylelint*, tsconfig.*, tsdoc.*, tslint*, tsup.config.*, turbo*, typedoc*, vercel*, vetur.config.*, vitest.config.*, webpack.config.*, workspace.json, xo.config.*, yarn*, graphql.config*, redwood.toml, server.config.js",
"readme.*": "authors, backers.md, changelog*, citation*, code_of_conduct.md, codeowners, contributing.md, contributors, copying, credits, governance.md, history.md, license*, maintainers, readme*, security.md, sponsors.md",
"storybook.config.js": "storybook.*.js",
"App.*": "App.*, i18n.*, index.html",
}
}