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

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.

NameDescriptionDefault
organizationAssert the user either does or does not have an organization.true
rolesCheck whether or not a user has the given role(s).-
subscribedAssert 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",
  }
}