From ee8f3bc8549528e14c50bb2933a43f01ef578d2a Mon Sep 17 00:00:00 2001 From: itsmegood Date: Tue, 30 Jan 2024 22:21:31 +0530 Subject: [PATCH] remove docs --- docs/README.md | 35 -- docs/apis.md | 21 - docs/authentication.md | 55 --- docs/caching.md | 84 ---- docs/client-hints.md | 13 - docs/community.md | 21 - docs/database.md | 384 ------------------ docs/decisions/000-template.md | 12 - docs/decisions/001-typescript-only.md | 44 -- docs/decisions/002-email-service.md | 41 -- docs/decisions/003-sqlite.md | 83 ---- docs/decisions/004-github-actions.md | 49 --- docs/decisions/005-client-pref-cookies.md | 94 ----- docs/decisions/006-native-esm.md | 38 -- docs/decisions/007-sessions.md | 44 -- docs/decisions/008-content-security-policy.md | 32 -- docs/decisions/009-region-selection.md | 32 -- docs/decisions/010-memory-swap.md | 41 -- docs/decisions/011-sitemaps.md | 29 -- docs/decisions/012-cuid.md | 81 ---- docs/decisions/013-email-code.md | 40 -- docs/decisions/014-totp.md | 114 ------ docs/decisions/015-monitoring.md | 36 -- docs/decisions/016-source-maps.md | 70 ---- docs/decisions/017-resend-email.md | 29 -- docs/decisions/018-images.md | 61 --- docs/decisions/019-components.md | 80 ---- docs/decisions/020-icons.md | 42 -- docs/decisions/021-node-version.md | 58 --- docs/decisions/022-report-only-csp.md | 31 -- docs/decisions/023-route-based-dialogs.md | 45 -- docs/decisions/024-change-email.md | 68 ---- docs/decisions/025-rate-limiting.md | 62 --- docs/decisions/026-path-aliases.md | 36 -- docs/decisions/027-toasts.md | 36 -- docs/decisions/028-permissions-rbac.md | 84 ---- docs/decisions/029-remix-auth.md | 22 - docs/decisions/030-github-auth.md | 69 ---- docs/decisions/031-imports.md | 76 ---- docs/decisions/032-csrf.md | 33 -- docs/decisions/033-honeypot.md | 35 -- docs/decisions/034-source-maps.md | 24 -- docs/decisions/035-remove-csrf.md | 43 -- docs/decisions/README.md | 7 - docs/deployment.md | 143 ------- docs/email.md | 22 - docs/examples.md | 119 ------ docs/features.md | 55 --- docs/fonts.md | 152 ------- docs/getting-started.md | 60 --- docs/guiding-principles.md | 30 -- docs/icons.md | 41 -- docs/managing-updates.md | 175 -------- docs/memory.md | 17 - docs/monitoring.md | 41 -- docs/permissions.md | 42 -- docs/redirects.md | 88 ---- docs/routing.md | 145 ------- docs/secrets.md | 37 -- docs/security.md | 85 ---- docs/seo.md | 41 -- docs/server-timing.md | 86 ---- docs/testing.md | 48 --- docs/timezone.md | 37 -- docs/toasts.md | 62 --- docs/troubleshooting.md | 47 --- 66 files changed, 4007 deletions(-) delete mode 100644 docs/README.md delete mode 100644 docs/apis.md delete mode 100644 docs/authentication.md delete mode 100644 docs/caching.md delete mode 100644 docs/client-hints.md delete mode 100644 docs/community.md delete mode 100644 docs/database.md delete mode 100644 docs/decisions/000-template.md delete mode 100644 docs/decisions/001-typescript-only.md delete mode 100644 docs/decisions/002-email-service.md delete mode 100644 docs/decisions/003-sqlite.md delete mode 100644 docs/decisions/004-github-actions.md delete mode 100644 docs/decisions/005-client-pref-cookies.md delete mode 100644 docs/decisions/006-native-esm.md delete mode 100644 docs/decisions/007-sessions.md delete mode 100644 docs/decisions/008-content-security-policy.md delete mode 100644 docs/decisions/009-region-selection.md delete mode 100644 docs/decisions/010-memory-swap.md delete mode 100644 docs/decisions/011-sitemaps.md delete mode 100644 docs/decisions/012-cuid.md delete mode 100644 docs/decisions/013-email-code.md delete mode 100644 docs/decisions/014-totp.md delete mode 100644 docs/decisions/015-monitoring.md delete mode 100644 docs/decisions/016-source-maps.md delete mode 100644 docs/decisions/017-resend-email.md delete mode 100644 docs/decisions/018-images.md delete mode 100644 docs/decisions/019-components.md delete mode 100644 docs/decisions/020-icons.md delete mode 100644 docs/decisions/021-node-version.md delete mode 100644 docs/decisions/022-report-only-csp.md delete mode 100644 docs/decisions/023-route-based-dialogs.md delete mode 100644 docs/decisions/024-change-email.md delete mode 100644 docs/decisions/025-rate-limiting.md delete mode 100644 docs/decisions/026-path-aliases.md delete mode 100644 docs/decisions/027-toasts.md delete mode 100644 docs/decisions/028-permissions-rbac.md delete mode 100644 docs/decisions/029-remix-auth.md delete mode 100644 docs/decisions/030-github-auth.md delete mode 100644 docs/decisions/031-imports.md delete mode 100644 docs/decisions/032-csrf.md delete mode 100644 docs/decisions/033-honeypot.md delete mode 100644 docs/decisions/034-source-maps.md delete mode 100644 docs/decisions/035-remove-csrf.md delete mode 100644 docs/decisions/README.md delete mode 100644 docs/deployment.md delete mode 100644 docs/email.md delete mode 100644 docs/examples.md delete mode 100644 docs/features.md delete mode 100644 docs/fonts.md delete mode 100644 docs/getting-started.md delete mode 100644 docs/guiding-principles.md delete mode 100644 docs/icons.md delete mode 100644 docs/managing-updates.md delete mode 100644 docs/memory.md delete mode 100644 docs/monitoring.md delete mode 100644 docs/permissions.md delete mode 100644 docs/redirects.md delete mode 100644 docs/routing.md delete mode 100644 docs/secrets.md delete mode 100644 docs/security.md delete mode 100644 docs/seo.md delete mode 100644 docs/server-timing.md delete mode 100644 docs/testing.md delete mode 100644 docs/timezone.md delete mode 100644 docs/toasts.md delete mode 100644 docs/troubleshooting.md diff --git a/docs/README.md b/docs/README.md deleted file mode 100644 index e330723..0000000 --- a/docs/README.md +++ /dev/null @@ -1,35 +0,0 @@ -# Epic Stack Documentation - -The goal of The Epic Stack is to provide solid opinions for teams to hit the -ground running on their web applications. - -We recommend you watch Kent's introduction to the Epic Stack to get an -understanding of the "why" behind the Stack: - -[![Epic Stack Talk slide showing Flynn Rider with knives, the text "I've been around and I've got opinions" and Kent speaking in the corner](https://github-production-user-asset-6210df.s3.amazonaws.com/1500684/277818553-47158e68-4efc-43ae-a477-9d1670d4217d.png)](https://www.epicweb.dev/talks/the-epic-stack) - -More of a reader? Read [the announcement post](https://epicweb.dev/epic-stack) -or -[an AI generated summary of the video](https://www.summarize.tech/www.youtube.com/watch?v=yMK5SVRASxM). - -This stack is still under active development. Documentation will rapidly improve -in the coming weeks. Stay tuned! - -# Top Pages - -- [Getting Started](./getting-started.md) - Instructions for how to get started - with the Epic Stack. -- [Features](./features.md) - List of features the Epic Stack provides out of - the box. -- [Deployment](./deployment.md) - If you skip the deployment step when starting - your app, these are the manual steps you can follow to get things up and - running. -- [Decisions](./decisions/README.md) - The reasoning behind various decisions - made for the Epic Stack. A good historical record. -- [Guiding Principles](./guiding-principles.md) - The guiding principles behind - the Epic Stack. -- [Examples](./examples.md) - Examples of the Epic Stack with various tools. - Most new feature requests people have for the Epic Stack start as examples - before being integrated into the framework. -- [Managing Updates](./managing-updates.md) - How to manage updates to the Epic - Stack for both the generated stack code as well as npm dependencies. diff --git a/docs/apis.md b/docs/apis.md deleted file mode 100644 index 415ade0..0000000 --- a/docs/apis.md +++ /dev/null @@ -1,21 +0,0 @@ -# APIs - -Remix routes have the ability to handle both backend code and UI code in the -same file. Remix `loader`s and `action`s are backend code that's tightly coupled -to the UI code for that route. - -Additionally, you can define routes that don't have any UI at all. These are -called [resource routes](https://remix.run/docs/en/main/guides/resource-routes). -This allows you to create REST endpoints or a GraphQL endpoint to make your app -data and logic consumable by third parties or additional clients (like a mobile -app). You can also use this to generate PDFs, images, stream multi-media and -more. - -The Epic Stack has a few resource routes in place for managing images, the -cache, and even has a few -["full stack components"](https://www.epicweb.dev/full-stack-components) for -components that manage the connection with their associated backend code. -[Watch the talk](https://www.youtube.com/watch?v=30HAT5Quvgk&list=PLV5CVI1eNcJgNqzNwcs4UKrlJdhfDjshf). - -So, yes, you can absolutely use the Epic Stack to build APIs for consumption by -third party clients. diff --git a/docs/authentication.md b/docs/authentication.md deleted file mode 100644 index 683257b..0000000 --- a/docs/authentication.md +++ /dev/null @@ -1,55 +0,0 @@ -# Authentication - -The Epic Stack manages its own authentication using web standards and -established libraries and tools. - -By default, the Epic Stack offers you two mechanisms for authentication: - -1. Username and password authentication -2. Provider authentication - -## Username and password authentication - -When a user wishes to sign up for an account, they are asked for their email -address. The Epic Stack will send them an email with a code as well as a link. -The user can then enter the code or click the link to verify their email address -which takes them through the onboarding flow which will allow them to set their -username and password. - -The password is stored using the [bcrypt](https://npm.im/bcrypt) algorithm. - -## Provider authentication - -The Epic Stack ships with a system for third party authentication allowing you -to easily add SSO (Single Sign On) to your application. The Epic Stack ships -with support for GitHub OAuth2 authentication out of the box. But you can easily -remove that and/or add other providers. It's all built using -[`remix-auth`](https://npm.im/remix-auth), so any provider supported there, can -be added, including [`web-oidc`](https://npm.im/web-oidc) which handles OpenID -Connect authentication and exports a `remix-auth` compatible auth strategy. - -You can check [this example](https://github.com/kentcdodds/epic-oidc) which -shows using OpenID Connect to add Google authentication to the Epic Stack. You -can expand beyond this to add any other provider you'd like, and if you need to -support SAML, you may look into -[`@boxyhq/remix-auth-sso`](https://github.com/boxyhq/remix-auth-sso). - -## TOTP and Two-Factor Authentication - -Two factor authentication is built-into the Epic Stack. It's managed using a the -[`@epic-web/totp`](https://npm.im/@epic-web/totp) (Time-based One Time -Passwords) utility. - -You can read more about the decision to use TOTP in -[the totp decision document](./decisions/014-totp.md). The secret and other -pertinent information is stored in a `verification` model (check the Prisma -schema). This verification model is used as the basis for all TOTP secrets. This -is used for non-expiring Two-Factor Authentication secrets as well as temporary -TOTP codes which are emailed to verify a user's ownership of an email/account. -So it's used for onboarding, forgot password, and change email flows. - -When a user has 2FA enabled on their account, they also are required to enter -their 2FA code within 2 hours of performing destructive actions like changing -their email or disabling 2FA. This time is controlled by the -`shouldRequestTwoFA` utility in the `login` full stack component in the resource -routes. diff --git a/docs/caching.md b/docs/caching.md deleted file mode 100644 index 31d902c..0000000 --- a/docs/caching.md +++ /dev/null @@ -1,84 +0,0 @@ -# Caching - -The Epic Stack comes with caching utilities and a management dashboard that -allows you to view and clear your cache. There are two caches built into the -Epic Stack: - -- **SQLite**: This is a separate database from the main application database. - It's managed by LiteFS so the data is replicated across all instances of your - app. This can be used for long-lived cached values. -- **LRU**: This is an in-memory cache that is used to store the results of - expensive queries or help deduplicate requests for data. It's not replicated - across instances and as it's in-memory it will be cleared when your app is - restarted. So this should be used for short-lived cached values. - -Caching is intended to be used for data that is expensive and/or slow to compute -or retrieve. It can help you avoid costs or rate limits associated with making -requests to third parties. - -It's important to note that caching should not be the first solution to slowness -issues. If you've got a slow query, look into optimizing it with database -indexes before caching the results. - -## Using the cache - -You won't typically interact directly with the caches. Instead, you will use -[`cachified`](https://npm.im/cachified) which is a nice abstraction for cache -management. We have a small abstraction on top of it which allows you to pass -`timings` to work seamlessly with -[the server timing utility](./server-timing.md). - -Let's say we're making a request to tito to get a list of events. Tito's API is -kinda slow and our event details don't change much so we're ok speeding things -up by caching them and utilizing the stale-while-revalidate features in -cachified. Here's how you would use cachified to do this: - -```tsx -import { cachified, cache } from '#app/utils/cache.server.ts' -import { type Timings } from '#app/utils/timing.server.ts' - -const eventSchema = z.object({ - /* the schema for events */ -}) - -export async function getScheduledEvents({ - timings, -}: { - timings?: Timings -} = {}) { - const scheduledEvents = await cachified({ - key: 'tito:scheduled-events', - cache, - timings, - getFreshValue: () => { - // do a fetch request to the tito API and stuff here - return [ - /* the events you got from tito */ - ] - }, - checkValue: eventSchema.array(), - // Time To Live (ttl) in milliseconds: the cached value is considered valid for 24 hours - ttl: 1000 * 60 * 60 * 24, - // Stale While Revalidate (swr) in milliseconds: if the cached value is less than 30 days - // expired, return it while fetching a fresh value in the background - staleWhileRevalidate: 1000 * 60 * 60 * 24 * 30, - }) - return scheduledEvents -} -``` - -With this setup, the first time you call `getScheduledEvents` it will make a -request to the tito API and return the results. It will also cache the results -in the `cache` (which is the SQLite cache). The next time you call -`getScheduledEvents` it will return the cached value if the cached value is less -than 30 days old. If the cached value is older than 24 hours, it will also make -a request to the tito API. If the cache value is more than 30 days old, it will -wait until the tito request is complete and then return the fresh value. - -Bottom line: You make the request much less often and users are never waiting -for it. Every situation will require you think through the implications of -caching and acceptable stale-ness, but the point is you have those levers to -pull. - -A lot more needs to be said on this subject (an entire workshop full!), but this -should be enough to get you going! diff --git a/docs/client-hints.md b/docs/client-hints.md deleted file mode 100644 index 2dc2641..0000000 --- a/docs/client-hints.md +++ /dev/null @@ -1,13 +0,0 @@ -# Client Hints - -> **NOTE:** Find background on this concept in the decision document: -> `0005-client-pref-cookies.md`. - -[Watch the tip](https://www.epicweb.dev/tips/use-client-hints-to-eliminate-content-layout-shift) -on [EpicWeb.dev](https://www.epicweb.dev): - -[![Kent smiling with VSCode showing code in the client-hints.tsx file](https://github.com/epicweb-dev/epic-stack/assets/1500684/ede18d0a-c117-4c65-9f1e-a87f262e4ce1)](https://www.epicweb.dev/tips/use-client-hints-to-eliminate-content-layout-shift) - -This functionality has been moved into `@epic-web/client-hints` and is the -primary source for documentation on this feature. Our integration can be found -in `app/utils/client-hints.tsx`. diff --git a/docs/community.md b/docs/community.md deleted file mode 100644 index 9d0b813..0000000 --- a/docs/community.md +++ /dev/null @@ -1,21 +0,0 @@ -# Community - -Here you can find useful learning resources and tools built and maintained by -the community, such as libraries, examples, articles, and videos. - -## Learning resources - -The primary learning resources for the Epic Stack is -[EpicWeb.dev](https://www.epicweb.dev), [EpicReact.dev](https://epicreact.dev), -and [TestingJavaScript.com](https://testingjavascript.com). On these you will -find free and paid premium content that will help you build epic web -applications (with or without the Epic Stack). - -The community has put together some additional learning resources that you may -enjoy! - -### Videos - -- **Dark Mode Toggling using Client-preference cookies** by - [@rajeshdavidbabu](https://github.com/rajeshdavidbabu) - Youtube - [link](https://www.youtube.com/watch?v=UND-kib_iw4) diff --git a/docs/database.md b/docs/database.md deleted file mode 100644 index b8acb5f..0000000 --- a/docs/database.md +++ /dev/null @@ -1,384 +0,0 @@ -# Database - -## Primary Instance - -The way that LiteFS works is that it has a "primary instance" that is the only -instance that can write to the database. All other instances are "replicas" that -can only read from the database. This is a pretty common pattern for databases -and it's how Fly's Postgres service works as well. - -The primary instance is determined by Fly's [consul](https://www.consul.io/) -service and can change over time. By default, all instances are allowed to be -primary instances. However, writes will be slower for people who are not -geographically close to the primary instance, so the Epic Stack has configured -consul to only allow instances in the primary region and it's recommended that -you choose a primary region that's closest to most of your users. This -configuration can be found in `other/litefs.yml`. The `PRIMARY_REGION` is -determined from the `primary_region` property in the `fly.toml` file. - -When you initialize an Epic Stack app, it will ask you which region you wish to -deploy to and this will set the `primary_region` in the `fly.toml` file. It is -recommended that you deploy two instances in this region so you have zero -downtime deploys. - -You can determine which instance is the primary instance by running the -following command: - -```sh -fly status --app [YOUR_APP_NAME] -``` - -This will show you a table of all your instances and the primary instance will -have "ROLE" set to "primary". - -To deploy more regions, you can run `fly scale count`. For example, if I wanted -to have two instances in `sjc` (my primary region) and one in `ams`, I would -run: - -```sh -fly scale count 2 --region sjc -fly scale count 1 --region ams -``` - -## Connecting to your production database - -The location of the sqlite database is kinda funny. The real location is in -`/data/litefs/dbs/sqlite.db`. However, during development you connect to it via -the fake filesystem managed by LiteFS so it can propagate any changes to your -database to all replicas. - -So to connect to your database, you'll want to connect to it at -`/litefs/data/sqlite.db` in the deployed application. Because it is SQLite, you -cannot connect to it unless you're running a command-line session on the -machine. You can do this using `fly ssh console`. The Dockerfile simplifies this -further by adding a `database-cli` command. You can connect to the live database -by running `fly ssh console -C database-cli`. - -To connect to the deployed database from your local machine using Prisma Studio, -you can utilize Fly's `ssh` and `proxy` commands. - -- Run in one terminal the command to start Prisma Studio on your desired Fly app - ```sh - fly ssh console -C "npm run prisma:studio" --app [YOUR_APP_NAME] - ``` -- Run in a second terminal the command to proxy your local port 5556 to Prisma - Studio - ```sh - fly proxy 5556:5555 --app [YOUR_APP_NAME] - ``` - -If you have multiple instances of your app running, and you'd like to make edits -to your database, you will need to run `prisma:studio` on the primary instance. - -- Get a list of your app instances, the `ROLE` column will show which instance - is `primary` - ```sh - fly status --app [YOUR_APP_NAME] - ``` -- Run the console command with the `-s` select flag - ```sh - fly ssh console -C "npm run prisma:studio" -s --app [YOUR_APP_NAME] - ``` -- Use your arrow keys to select the primary instance - -To work with Prisma Studio and your deployed app's database, simply open -`http://localhost:5556` in your browser. - -> **Note**: You may want to add `--select` to the `fly ssh console` command to -> select the instance you want to connect to if you have multiple instances -> running. Otherwise you could connect to a non-primary instance. The easiest -> way to determine the primary instance (because it can change) is to run -> `fly status` and the row that has "ROLE" set to "primary" is the primary -> instance. Alternatively, you can open the deployed application and check the -> request headers. One of them will be `Fly-Primary-Instance` which will tell -> you the instance ID of the primary instance. - -## Migrations - -Thanks to Prisma, we've got a great mechanism for handling database migrations. -Any migrations necessary are run (by the primary instance only) as part of the -deploy process. You can find this in the `other/litefs.yml` file. - -We deploy to multiple instances at once and the way we deploy means we don't -have any downtime during deploys. However, to make this work, you do need to -make sure you can run two versions of your app at once. Specifically, you should -not deploy "breaking schema changes" to your app in a way that will break any -running instances. This is a pretty typical requirement for production -applications. - -The basic idea is that you maintain support for any client that is currently -running. In Epic Stack apps that's normally just the currently running version -of the app. So you really only need to worry about "breaking schema changes" -whenever you deploy a new version, but you don't have to worry about avoiding -breaking schema changes for old versions of the app that are no longer running. - -In practice, this means you should adopt a "widen then narrow" strategy for -schema migrations. This is a pretty common practice, but here's a simple example -of how this works (each step here is an individual deploy): - -1. Widen app to consume A or B -2. Widen db to provide A and B and the app to write to both A and B -3. Narrow app to consume B and only write to B -4. Narrow db to provide B - -So, let's say that today your app allows users to provide a "name" and you want -to change that to `firstName` and `lastName` instead. Here's how you'd do that -(again, each of these steps end in a deploy): - -1. Widen app to consume `firstName` and `lastName` or `name`. So all new code - that references the `firstName` and `lastName` fields should fallback to the - `name` field and not error if the `firstName` and `lastName` fields don't - exist yet, which it won't at this point. -2. Widen db to provide `firstName` and `lastName` and `name`. So the `name` - field should be populated with the `firstName` and `lastName` fields. You can - do this as part of the migration SQL script that you run. The easiest way to - do this is to generate the migration script to add the fields using - `prisma migrate` and then modify the script to copy the existing data in the - `name` field to the `firstName` field (maybe with the help of VSCode Copilot - πŸ˜…). -3. Narrow app to consume `firstName` and `lastName` by only writing to those - fields and removing the fallback to the `name` field. -4. Narrow db to provide `firstName` and `lastName` by removing the `name` field. - So now you can remove the `name` field from the db schema. - -By following this strategy, you can ensure zero downtime deploys and schema -migrations. - -## Seeding Production - -In this application we have Role-based Access Control implemented. We initialize -the database with `admin` and `user` roles with appropriate permissions. - -This is done in the `migration.sql` file that's included in the template. If you -need to seed the production database, modifying migration files manually is the -recommended approach to ensure it's reproducible. - -The trick is not all of us are really excited about writing raw SQL (especially -if what you need to seed is a lot of data), so here's an easy way to help out: - -1. Create a script very similar to our `prisma/seed.ts` file which creates all - the data you want to seed. -1. Run the script locally to generate the data you want to seed. -1. Create a "dump" of the seed database using the `sqlite3` command line tool. - ```sh nonumber - sqlite3 seed.db .dump > seed.sql - ``` -1. Copy the relevant bits from the `seed.sql` file into your `migration.sql` - file. The `seed.sql` will include create table/index lines etc. which should - already be in your `migration.sql`. You probably just want `INSERT` commands. -1. Deploy your app and verify that the data was seeded correctly. - -If your app has already applied all migrations, then the changes to the -`migration.sql` won't be applied (because prisma's already applied it). So then -you can run the following command to apply the migration: - -```sh nonumber -fly ssh console -C "npx prisma migrate reset --skip-seed --force" --app [YOUR_APP_NAME] -``` - -> **WARNING**: This will reset your database and apply all migrations. Continue -> reading if you want to avoid this. - -If you have existing data in your production database and you'd like to seed it -with more data without performing a migration, then it's a bit more involved. - -1. Backup your production database. -1. Create a new database file (locally) with the data you want to seed. -1. Create a "dump" of the seed database using the `sqlite3` command line tool. - ```sh nonumber - sqlite3 seed.db .dump > seed.sql - ``` -1. Copy the `seed.sql` file to your production volume next to your database (via - `fly sftp`) -1. SSH into your production server and run the following command: - ```sh nonumber - sqlite3 data.db < seed.sql - ``` -1. Verify that your production database has been seeded correctly. If it hasn't, - then restore your backup (asap). - -## Backups - -### LiteFS Cloud Backups - -LiteFS Cloud is a service offered by Fly.io for managing backup and restore -functionality. - -This is the simplest method for backing up your database. - -It offers the ability to restore your database to any point in time in the last -30 days, with 5 minute granularity. - -Fly.io has some great documentation on how to set this up: - -- [Pricing](https://fly.io/docs/about/pricing/#litefs-cloud) -- [LiteFS Cloud Setup](https://fly.io/docs/litefs/cloud-backups/) -- [Restoring DB with LiteFS Cloud](https://fly.io/docs/litefs/cloud-restore/) -- [Disaster Recovery with LiteFS Cloud](https://fly.io/docs/litefs/disaster-recovery/) - -The following is a summary of the steps to set up LiteFS Cloud: - -1. Create a LiteFS Cloud cluster in your Fly.io dashboard: - https://fly.io/dashboard/personal/litefs - - Take note of the auth token, you'll need it in the next step -1. Set the `LITEFS_CLOUD_TOKEN` to the token from your dashboard: - ```sh - fly secrets set LITEFS_CLOUD_TOKEN="LITEFS_CLOUD_TOKEN_HERE" --app [YOUR_APP_NAME] - ``` -1. You should now be able to restore backups from the LiteFS dashboard. - -### Manual DB Backups - -Manual DB backups can be taken/restored using `litefs` commands: - -- `litefs export`: https://fly.io/docs/litefs/export/ -- `litefs import`: https://fly.io/docs/litefs/import/ - -**Make sure to keep the backup in a secure location. Your DB backup will contain -user information and password hashes!** - -You can manually create a backup for your database using the following steps: - -1. SSH into your fly instance: - ```sh - fly ssh console --app [YOUR_APP_NAME] - ``` -1. Create a `backups` folder: - ```sh - mkdir /backups - ``` -1. Create a backup file using `litefs export`, and exit the console (it is - recommended to name the exported file with the current date): - ```sh - litefs export -name sqlite.db /backups/backup-2023-10-10.db - exit - ``` -1. Use sftp to download the backup file: - ```sh - fly ssh sftp get /backups/backup-2023-10-10.db --app [YOUR_APP_NAME] - ``` - -You can now store this backup file wherever you like, such as an S3 bucket -(again, make sure it's a secure location!). - -See the fly docs for more info: https://fly.io/docs/litefs/backup/ - -### Manual DB restoration - -**WARNING - THIS OVERWRITES YOUR DATABASE, YOU CAN LOSE DATA!! TAKE ANOTHER -BACKUP OF THE CURRENT DATABASE BEFORE DOING THIS!!** - -1. Establish an sftp session with the fly instance and upload the backup file to - the server using `put`: - ```sh - fly ssh sftp shell --app [YOUR_APP_NAME] - put backup-2023-10-10.db - ``` -1. Quit the sftp session with CTRL+C -1. SSH into the fly instance: - ```sh - fly ssh console --app [YOUR_APP_NAME] - ``` -1. Restore the database from the backup file using `litefs import` - ```sh - litefs import -name sqlite.db /backup-2023-10-10.db - ``` -1. Exit the ssh session - ```sh - exit - ``` - -## Troubleshooting - -### Faulty Prisma Migration - -If you accidentally run a faulty migration on prod DB, and you see this message -in the logs: - -```sh -migrate found failed migrations in the target database, new migrations will not be applied. Read more about how to resolve migration issues in a production database: https://pris.ly/d/migrate-resolve -``` - -You've got a few options: - -1. If you don't care about the data, you can delete the app on fly and re-deploy - it after deleting/fixing the faulty migration. -2. If you have a recent backup of the DB, you can restore it and re-deploy the - app after deleting/fixing the faulty migration. **(Make sure that the backup - isn't too old, otherwise you'll lose data)** - - You can fix a faulty migration by either editing the migration SQL file, or - by deleting the particular migration folder from `prisma/migrations` and - re-generating the migration after fixing the error. -3. If you do care about the data and don't have a backup, you can follow these - steps: - - 1. Comment out the - [`exec` section from `litefs.yml` file](https://github.com/epicweb-dev/epic-stack/blob/main/other/litefs.yml#L31-L37). - - ```yml - # exec: - # - cmd: node ./other/setup-swap.js - - # - cmd: npx prisma migrate deploy - # if-candidate: true - - # - cmd: npm start - ``` - - 2. Commit this change and deploy the app to fly. - - This will make sure that after building the dockerfile and deploying it - to the fly machine, the `node ./other/setup-swap.js`, - `npx prisma migrate deploy` and `npm start` commands won't be executed. - 3. Now that the main machine is up and running, you can SSH into it by - running `fly ssh console --app [YOUR_APP_NAME]` in the terminal. - 4. Create a backup of the DB and download it by following the steps mentioned - in the [Manual DB Backups](#manual-db-backups) section. - 5. Make a copy of the downloaded DB backup file and store it in a secure - location (If something were to go wrong, we have a backup of the backup). - 6. Write SQL or use a tool like - [DB Browser for SQLite](https://sqlitebrowser.org/) to remove the last - (failed) entry from `_prisma_migrations` table from the downloaded DB - backup file. - - If you're using DB Browser for SQLite but your DB isn't selectable when - selecting a file to open, change DB file extension to `sqlite` from - `db`. **Make sure to change it back to `db` before uploading it to the - fly machine**. - 7. Follow the steps mentioned in the - [Manual DB restoration](#manual-db-restoration) section to restore the - modified DB backup file to the fly machine. - 8. Now the DB is restored, but if you deploy the app with the faulty - migration, it will fail again. So you need to delete/fix the faulty - migration. - - You can fix a faulty migration by either editing the migration SQL file, - or by deleting the particular migration folder from `prisma/migrations` - and re-generating the migration after fixing the error. - 9. Uncomment the - [`exec` section from `litefs.yml` file](https://github.com/epicweb-dev/epic-stack/blob/main/other/litefs.yml#L31-L37). - - ```yml - exec: - - cmd: node ./other/setup-swap.js - - - cmd: npx prisma migrate deploy - if-candidate: true - - - cmd: npm start - ``` - - 10. Commit this change and deploy the app to fly :tada: - - NOTE: Steps 5-7 can be skipped if you can write SQL to remove the faulty - migration from the DB. You can use the following SQL query to remove the - faulty migration from the DB: - - ```sql - DELETE FROM _prisma_migrations WHERE name = 'migration_name'; - ``` - - Make sure to replace `migration_name` with the name of the faulty migration. - -This is particularly hard because of how CONSUL leases work. You cannot just -simply spin up another machine and access your database from it, because only -one machine can get the primary lease. So you have to disable exec commands so -the machine that has primary lease can start up, then you can SSH into it and -fix the DB. diff --git a/docs/decisions/000-template.md b/docs/decisions/000-template.md deleted file mode 100644 index 374aa1c..0000000 --- a/docs/decisions/000-template.md +++ /dev/null @@ -1,12 +0,0 @@ -# Title - -Date: YYYY-MM-DD - -Status: proposed | rejected | accepted | deprecated | … | superseded by -[0005](0005-example.md) - -## Context - -## Decision - -## Consequences diff --git a/docs/decisions/001-typescript-only.md b/docs/decisions/001-typescript-only.md deleted file mode 100644 index f28ebd8..0000000 --- a/docs/decisions/001-typescript-only.md +++ /dev/null @@ -1,44 +0,0 @@ -# TypeScript Only - -Date: 2023-05-08 - -Status: accepted - -## Context - -The `create-remix` CLI allows users to select whether they want to use -JavaScript instead of TypeScript. This will auto-convert everything to -JavaScript. - -There is (currently) no way to control this behavior. - -Teams and individuals building modern web applications have many great reasons -to build them with TypeScript. - -One of the challenges with TypeScript is getting it configured properly. This is -not an issue with a stack which starts you off on the right foot without needing -to configure anything. - -Another challenge with TypeScript is handling dependencies that are not written -in TypeScript. This is increasingly becoming less of an issue with more and more -dependencies being written in TypeScript. - -## Decision - -We strongly advise the use of TypeScript even for simple projects and those -worked on by single developers. So instead of working on making this project -work with the JavaScript option of the `create-remix` CLI, we've decided to -throw an error informing the user to try again and select the TypeScript option. - -We've also made the example script in the `README.md` provide a selected option -of `--typescript` so folks shouldn't even be asked unless they leave off that -flag in which case our error will be thrown. - -## Consequences - -This makes the initial experience not great for folks using JavaScript. -Hopefully the Remix CLI will eventually allow us to have more control over -whether that question is asked. - -This also may anger some folks who really don't like TypeScript. For those -folks, feel free to fork the starter. diff --git a/docs/decisions/002-email-service.md b/docs/decisions/002-email-service.md deleted file mode 100644 index 17bc397..0000000 --- a/docs/decisions/002-email-service.md +++ /dev/null @@ -1,41 +0,0 @@ -# Email Service - -Date: 2023-05-08 - -Status: superseded by [017](017-resend-email.md) - -## Context - -When you're building a web application, you almost always need to send emails -for various reasons. Packages like `nodemailer` make it quite easy to send your -own emails through your own mailserver or a third party's SMTP server as well. - -Unfortunately, -[deliverability will suffer if you're not using a service](https://cfenollosa.com/blog/after-self-hosting-my-email-for-twenty-three-years-i-have-thrown-in-the-towel-the-oligopoly-has-won.html). -The TL;DR is you either dedicate your company's complete resources to "play the -game" of email deliverability, or you use a service that does. Otherwise, your -emails won't reliably make it through spam filters (and in some cases it can -just get deleted altogether). - -[The guiding principles](https://github.com/epicweb-dev/epic-stack/blob/main/docs/guiding-principles.md) -discourage services and encourage quick setup. - -## Decision - -We will use a service for sending email. If emails don't get delivered then it -defeats the whole purpose of sending email. - -We selected [Mailgun](https://www.mailgun.com/) because it has a generous free -tier and has proven itself in production. However, to help with quick setup, we -will allow deploying to production without the Mailgun environment variables set -and will instead log the email to the console so during the experimentation -phase, developers can still read the emails that would have been sent. - -During local development, the Mailgun APIs are mocked and logged in the terminal -as well as saved to the fixtures directory for tests to reference. - -## Consequences - -Developers will need to either sign up for Mailgun or update the email code to -use another service if they prefer. Emails will actually reach their -destination. diff --git a/docs/decisions/003-sqlite.md b/docs/decisions/003-sqlite.md deleted file mode 100644 index 0a78a26..0000000 --- a/docs/decisions/003-sqlite.md +++ /dev/null @@ -1,83 +0,0 @@ -# SQLite - -Date: 2023-05-13 - -Status: accepted - -## Context - -SQLite is a sql-based database with a particularly unique feature: the entire -database is in a single file. Largely for this reason, I've historically seen it -as a simple database for simple use cases. - -However, in recent years SQLite has received a great deal of development and -attention making it a simple database for even more advanced use cases. The fact -that SQLite is a single file on disk comes with a major benefit of 0 latency -which -[drastically reduces the "n+1 problem"](https://www.sqlite.org/np1queryprob.html). - -Another issue is database size, however, -[SQLite is capable of handling databases that are an Exabyte in size](https://sqlite.org/hctree/doc/hctree/doc/hctree/index.html) -(that's one million Terabytes, or one billion Gigabytes 🀯). - -SQLite does not support subscriptions which can be a limitation on certain -real-time use cases. However, there are plenty of reasons to recommend against -using database subscriptions for real-time use cases anyway, and that is the -case in the Epic Stack (as of today, we don't have real-time examples or docs, -but when we do it's likely we wouldn't use database subscriptions anyway). - -SQLite being a file on disk does make connecting from external clients -effectively impossible. This makes it more difficult to connect to it using -database admin tools. However, it is possible to run tools like `prisma studio` -on the machine where the sqlite volume is mounted if necessary. And the -`Dockerfile` is already configured to allow easy SSH connections into the sqlite -CLI. This is nowhere near as good as a proper admin tool, but there's likely -more that could be done here to improve the experience. On my own website -(kentcdodds.com), I am able to have prisma studio run in production protected by -authentication and I expect we'll be able to add something like that to the Epic -Stack in the future. - -SQLite does not support plugins like -[TimescaleDB](https://github.com/timescale/timescaledb) for Postgres. While -time-series data is possible with SQLite, I do not have experience with this use -case and can't speak to the challenges there. My intuition says it's not -advisable to use SQLite for that use case. - -SQLite does not support enums which means you're forced to use strings. I have -mixed feelings about this, but I mostly don't like enums anyway. The main -drawback to this is when it comes to the typings for the client which doesn't -allow you to ensure all values of a column are only within a set of specific -possible values for the string. However, with Prisma client extensions, handling -this kind of enforcement at the client (and typing) level should be possible. -This would need to be documented in the future -[#29](https://github.com/epicweb-dev/epic-stack/issues/29). - -As a file on disk, you cannot "distribute" SQLite directly. However, with tools -like [Turso](https://turso.tech/) or [LiteFS](https://fly.io/docs/litefs), you -can make that work. And both of these tools even have built-in solutions to the -"Read Replica Consistency" challenge. So if you need your app to run in multiple -instances, you need to use one of these tools. - -Using SQLite is an _enormous_ simplification both during development and -production. With one less service to worry about (especially something as -critical as your database), you're less likely to experience an outage. -Additionally, all you need is a persisted volume for your application (which you -would need for a database service anyway), so it's less costly as well. - -## Decision - -We'll use SQLite because it satisfies the use cases of our target audience. - -## Consequences - -This means we need to have a way to connect to our SQLite database in -production. We'll also need to have a way to easily seed the database -(documentation will need to be written and perhaps some scripts). We want to -support multi-region so LiteFS needs to be configured. - -This also means real-time use cases will need to find a solution that doesn't -rely on database subscriptions. - -Ultimately, this decision drastically simplifies development, deployment, -maintenance, and services for the web application and reduces running costs. A -huge win for the majority of web applications. diff --git a/docs/decisions/004-github-actions.md b/docs/decisions/004-github-actions.md deleted file mode 100644 index 036e398..0000000 --- a/docs/decisions/004-github-actions.md +++ /dev/null @@ -1,49 +0,0 @@ -# GitHub Actions - -Date: 2023-05-15 - -Status: accepted - -## Context - -Deploying serious web applications to production on a regular basis requires -automation. Running testing, linting, and a build before deployment is a -accepted practice to ensure a quality product is deployed. - -Alongside this, we want to run this automation any time code is merged (or will -soon be merged) from contributors to the project. This is called "Continuous -Integration" and is necessary for teams to move confidently and focus on their -value proposition. Make the robots do the boring stuff so we can focus on the -creative work. - -We can run this automation on our own machines during development, but it can be -easy to forget to do this. It's even harder to be confident the automation was -run when you are trying to combine the work of multiple people (who's -responsible to run the deploy script?). So it's best to have this automation run -on a separate machine that's dedicated to this task. - -I don't want to have that machine running in my closet, so instead we need to -look to outside services for managing this for us. There are many such services. -Most of the target audience of the Epic Stack are familiar with GitHub and many -use it already for other projects. The vast majority already have accounts on -GitHub as well. - -GitHub has a CI service called -[GitHub Actions](https://docs.github.com/en/actions) which satisfies all the -necessary use cases for the Epic Stack. It does not require an additional -account, though it does necessitate you use GitHub for hosting the code. Also, -it is free for open source projects, but paid for private projects. - -## Decision - -We've decided to use GitHub Actions for Continuous Integration in the Epic -Stack. We have a single action that handles running linting, type checking, -tests, and deployment to both staging (`dev` branch) and production (`main` -branch). - -## Consequences - -This means users of the Epic Stack must host their repositories within GitHub -and pay for private repositories. This reduces the number of extra services -users of the Epic Stack need to sign up for since the vast majority already have -accounts with GitHub anyway. diff --git a/docs/decisions/005-client-pref-cookies.md b/docs/decisions/005-client-pref-cookies.md deleted file mode 100644 index 719a4a0..0000000 --- a/docs/decisions/005-client-pref-cookies.md +++ /dev/null @@ -1,94 +0,0 @@ -# Client Preference Cookies - -Date: 2023-05-16 - -Status: accepted - -## Context - -Server rendering is preferred for most things to avoid content layout shift -(which causes a poor user experience). Unfortunately, there are a number of -limitations with this because the browser doesn't give the server enough -information about the user's preferences. For example: - -- `prefers-color-scheme` (light/dark mode) -- `prefers-reduced-data` -- time zone offset -- locale - -And much more. - -The problem is that if what you display to the user relies on these values, then -what the server renders could be wrong and the user will see the incorrect UI -until client-side JavaScript can take over and correct it which causes a "Flash -of Incorrect UI." This is a terrible user experience. - -This is such an issue that the web platform will (hopefully soon) be adding new -user preferences headers to each request for us to know these values on the -server. Read, -[User preference media features client hints headers](https://web.dev/user-preference-media-features-headers/) -and -[User Locale Preferences](https://github.com/romulocintra/user-locale-client-hints). -However, there's no telling when these features will become standard and -implemented in all browsers Epic Stack apps target so we cannot rely on this or -wait for it. - -One solution I've used on my personal website is to include a tiny bit of inline -JavaScript in the client that runs before the user has a chance to see anything -to correct any flaws in the UI. This is normally enough, but on extremely slow -connections the user will still see a flash of incorrect UI. It's also quite -complex and pretty hacky. - -Another solution I've seen from -[Jacob Paris](https://www.jacobparis.com/content/remix-ssr-dates) is to simply -use cookies to solve this problem. You simply set a cookie in the browser from -the client for every property your application needs and then the server knows -the user preference during the document request and server render. There are two -limitations here: - -1. First time users won't have the cookie set -2. The cookie will be stale if the user changes their preference - -To solve the first problem, we can simply check that the cookies are set and if -they are not, then we instead send a minimal document that includes a tiny bit -of JavaScript that sets the cookies and then reloads the page. This is not -ideal, however it's effectively as harmful to the user as a redirect which many -websites do anyway (for example, go to `https://youtube.com` and you instantly -get redirected to `https://www.youtube.com`). Additionally, this is effectively -how -[the draft specification](https://wicg.github.io/user-preference-media-features-headers/#usage-example) -for the web platform's solution works as well. - -To solve the second problem, we can simply keep a tiny bit of JS in the head of -the document that does a quick check of the cookie values and if they are stale, -then it sets them again and triggers a reload. Still not ideal, but again, it's -better than a content layout shift. And hopefully this solution isn't permanent -and we can remove it once the web platform offers a better solution. - -To take things further, we can future proof this solution a bit by trying to -adhere to the web platform's proposed solution as closely as possible, so that -when it does become available, we can simply switch from the cookies to headers -and remove the JS, leaving us with few changes to make. - -## Decision - -Even though the web platform is working on a solution for this, we cannot wait -for it. Despite page reloads being a sub-optimal user experience, it's better -than the content layout shift (flash of incorrect UI) alternative. Therefore, we -will use cookies and reloads to solve this problem. - -## Consequences - -The user's first page load will be a bit slower than normal (as will any page -load after their preferences change) because we have to do a page reload to set -the cookies. However, this is a one-time cost and the user will not experience -this again until they change their preferences. - -The user will not experience content layout shift for any user preferences our -app depends on for the server render. This is a huge win for user experience. - -If a user has cookies disabled, then we need to detect that and fallback to -default values. Users in this situation will experience content layout shift, -but there's nothing else we can do about that. Additionally, users who have -cookies disabled will not be able to authenticate which is a separate concern -that should be addressed in a different decision document. diff --git a/docs/decisions/006-native-esm.md b/docs/decisions/006-native-esm.md deleted file mode 100644 index fbb941b..0000000 --- a/docs/decisions/006-native-esm.md +++ /dev/null @@ -1,38 +0,0 @@ -# Native ESM - -Date: 2023-05-18 - -Status: accepted - -## Context - -Oh boy, where do I start? The history of JavaScript modules is long and -complicated. I discuss this a bit in my talk -[More than you want to know about ES6 Modules](https://kentcdodds.com/talks/more-than-you-want-to-know-about-es-6-modules). -Many modern packages on npm are now publishing esm-only versions of their -packages. This is fine, but it does mean that using them from a CommonJS module -system requires dynamic imports which is limiting. - -In Remix v2, ESM will be the default behavior. Everywhere you look, ESM is -becoming more and more the standard module option. CommonJS modules aren't going -anywhere, but it's a good idea to stay on top of the latest. - -Sadly, this is a bit of a "who moved my cheese" situation. Developers who are -familiar with CommonJS modules will be annoyed by things they were used to doing -in CJS that they can't do the same way in ESM. The biggest is dynamic (and -synchronous) requires. Another is the way that module resolution changes. There -are some packages which aren't quite prepared for ESM and therefore you end up -having to import their exports directly from the files (like radix for example). -This is hopefully a temporary problem. - -## Decision - -We're adopting ESM as the default module system for the Epic Stack. - -## Consequences - -Experienced developers will hit a couple bumps along the way as they change -their mental model for modules. But it's time to do this. - -Some tools aren't very ergonomic with ESM. This will hopefully improve over -time. diff --git a/docs/decisions/007-sessions.md b/docs/decisions/007-sessions.md deleted file mode 100644 index 4e41755..0000000 --- a/docs/decisions/007-sessions.md +++ /dev/null @@ -1,44 +0,0 @@ -# Sessions - -Date: 2023-05-25 - -Status: accepted - -## Context - -There are a lot of ways to manage authentication in applications on the web. -We've chosen to use cookies to identify a user. However, you can just stick the -user's ID in that cookie and then sign it with a secret and then you'll always -know that the user ID in the cookie is legitimate and you won't need to go to -the database at all to determine who is making the request (or, at least to know -their ID). - -The limitation here is that once a client has a signed cookie with the user ID, -it cannot be revoked. You can set a `maxAge` or a `expires`, but you can't -proactively revoke it. There's also not a great way to know how many sessions -are currently active for a given user. - -For many applications, being able to proactively invalidate sessions is -necessary. My own site has this capability. Many sites will even associate some -identifiable information about each session as well as the last time that -session was used to display that information to the user and allow them to -revoke specific sessions. - -## Decision - -We will add a sessions table to the built-in template's schema allowing a -capability for a simple "revoke all" feature. Adding more information to the -session model would be simple for anyone needing a more sophisticated session -management strategy. This simple implementation gets people on the right foot -with regards to session management. - -## Consequences - -This requires doing a database lookup for every authenticated request to -determine whether there is an active session in the database for the user's -request. This is not a problem for us since we're using SQLite and there's 0 -latency, so the query is extremely fast. - -The sessions table does not currently have the capability to proactively delete -expired sessions which means it could fill up indefinitely. This would be a good -problem for built-in cron job support to solve eventually. diff --git a/docs/decisions/008-content-security-policy.md b/docs/decisions/008-content-security-policy.md deleted file mode 100644 index f29216e..0000000 --- a/docs/decisions/008-content-security-policy.md +++ /dev/null @@ -1,32 +0,0 @@ -# Content Security Policy - -Date: 2023-05-27 - -Status: accepted - -Update: [022-report-only-csp.md](./022-report-only-csp.md) - -## Context - -A Content Security Policy (CSP) allows a server to inform the browser about the -sources from which it expects to load resources. This helps to prevent -cross-site scripting (XSS) attacks by not allowing the browser to load resources -from any other location than the ones specified in the CSP. - -CSPs that are overly strict can be a major pain to work with, especially when -using third-party libraries. Still, for the most security, the CSP should be as -strict as possible. Additional sources can be added to the CSP as needed. - -## Decision - -We configure a tight CSP for the default application using -[helmet](https://npm.im/helmet) which is a de-facto standard express middleware -for configuring security headers. - -## Consequences - -Applications using the Epic Stack will start with a safer default configuration -for their CSP. It's pretty simple to add additional sources to the CSP as -needed, but it could definitely be confusing for folks who are unaware of the -CSP to load resources. Documentation will be needed to help people understand -what to do when they get CSP errors. diff --git a/docs/decisions/009-region-selection.md b/docs/decisions/009-region-selection.md deleted file mode 100644 index 6a04ba8..0000000 --- a/docs/decisions/009-region-selection.md +++ /dev/null @@ -1,32 +0,0 @@ -# Region Selection - -Date: 2023-06-02 - -Status: accepted - -## Context - -Fly supports running your app in -[34 regions](https://fly.io/docs/reference/regions/) all over the world. The -Epic Stack is set up to allow you to run in as many of these regions as you -like, but for cost reasons, it's best to start out with a single region until -your app needs that level of scale. - -Region selection has an important impact on the performance of your app. When -you're choosing a single region, you're choosing who your app is going to be -slower for. So you really should choose the region that's closest to the most -critical/closest users. - -Unfortunately, there's no way for us to know this for every app. We can't just -select a region for you. And we also can't just select the region that's closest -to you. We need you to actually think about and make this decision. - -## Decision - -Ask which region the app should be deployed to during setup. - -## Consequences - -Forces the developer to make a choice (goes against the "Minimize Setup -Friction" guiding principle). However, we can make it slightly better by -defaulting to the region that's closest to the developer. diff --git a/docs/decisions/010-memory-swap.md b/docs/decisions/010-memory-swap.md deleted file mode 100644 index a85e25f..0000000 --- a/docs/decisions/010-memory-swap.md +++ /dev/null @@ -1,41 +0,0 @@ -# Memory Swap - -Date: 2023-06-02 - -Status: accepted - -## Context - -Node.js based apps can use a lot of memory. And while we can scale up the memory -on the instances that run your app, we can't scale it up infinitely. Especially -when we want to be cost sensitive. So we need to be able to handle the case -where your app uses more memory than is available on the instance. A solution to -this is to use swap memory. - -Swap memory is a way to use disk space as memory. It's not as fast as real -memory, but it's better than crashing. And it's a lot cheaper than scaling up -the memory on your instances. It makes sense for many types of apps (even at -scale) to use swap memory. Especially for apps just getting off the ground, -making use of swap memory can be a great way to keep costs down. - -Because our app is running in a container with a mounted volume, we can't use -the normal swap memory mechanisms. Instead, we need to use a swap file. This -means we need to create a file on the mounted volume and then use that file as -swap memory using `fallocate`, `mkswap`, and `swapon`. - -Size of the swap file is pretty subjective to the application and situation. The -Epic Stack app memory starts at 256MB on Fly. Based on that amount of memory, a -good rule of thumb for the size of the swap file is 2-4x the size of memory, -which would put the swap file at 512MB-1GB (for a 2GB+ RAM system, you typically -want the swap file to be the same size as the memory). Considering our volumes -are set to 1GB for starters, we'll start with a 512MB swap file. - -## Decision - -During app startup, we'll create a swap file on the mounted volume and then use -that file as swap memory for the application. - -## Consequences - -In high utilization situations, we will have degraded performance instead of a -crash. This is a good tradeoff for most apps. diff --git a/docs/decisions/011-sitemaps.md b/docs/decisions/011-sitemaps.md deleted file mode 100644 index 09a7703..0000000 --- a/docs/decisions/011-sitemaps.md +++ /dev/null @@ -1,29 +0,0 @@ -# Sitemaps - -Date: 2023-06-05 - -Status: accepted - -## Context - -[Sitemaps](https://developers.google.com/search/docs/crawling-indexing/sitemaps/overview) -are useful to help website crawlers (like search engines) find all the content -on your website. Most of the time they aren't necessary if you're linking -between pages well. However, for large websites with lots of content that are -highly search engine sensitive, they can be useful. - -It's normally not a big deal to get them wrong if you don't care about it, but -if you really don't care about it, having the code for it can get in the way and -it's kind of annoying. - -## Decision - -Instead of building a sitemap into the template, we'll use -[an example](/docs/examples.md) people can reference to add a sitemap to their -Epic Stack sites if they like. - -## Consequences - -This turns sitemaps into an opt-in for developers using the Epic Stack. Most -people using the Epic Stack probably don't need a sitemap, and those who do will -only need a few minutes of following the example to get it working. diff --git a/docs/decisions/012-cuid.md b/docs/decisions/012-cuid.md deleted file mode 100644 index bd35860..0000000 --- a/docs/decisions/012-cuid.md +++ /dev/null @@ -1,81 +0,0 @@ -# CUID - -Date: 2023-06-06 - -Status: accepted - -## Context - -I noticed there were a couple models that were using `uuid`. To verify my -understanding of the `uuid` vs `cuid` debate, I took it to ChatGPT. - -
-ChatGPT conversation: CUID vs UUID Debate - -**CUID vs UUID Debate** - -[June 6, 2023](https://chat.openai.com/share/cc0eb433-9bc0-48ea-861a-dfc7ed5acf43) - -**Kent:** Please argue both sides of the cuid vs uuid debate. - -**ChatGPT:** Sure! Let's explore both sides of the debate between CUID -(Collision-resistant Unique ID) and UUID (Universally Unique Identifier). - -Arguments in favor of CUID: - -1. Simplicity and readability: CUIDs are designed to be simple and - human-readable, consisting of a combination of lowercase letters and - numbers. This makes them easier to work with and understand compared to the - alphanumeric strings of UUIDs. -2. Shorter length: CUIDs are typically shorter in length compared to UUIDs, - which can be advantageous in certain scenarios where storage or transmission - efficiency is a concern. The shorter length can lead to reduced memory usage - and faster processing times. -3. Collision resistance: CUIDs are specifically designed to minimize the - chances of collision, meaning the probability of generating duplicate IDs is - extremely low. This makes them suitable for scenarios where uniqueness is - crucial, such as database keys or distributed systems. - -Arguments in favor of UUID: - -1. Universality and standardization: UUIDs are widely recognized and used - across different systems and programming languages. They have a standardized - format and are supported by various libraries and frameworks, making them - easier to integrate into existing systems and ensure compatibility. -2. Higher entropy and randomness: UUIDs typically have a higher degree of - randomness due to the inclusion of a timestamp and a unique identifier from - the host or network. This can be beneficial in situations where security and - unpredictability are essential, such as generating secure session tokens or - cryptographic keys. -3. Widely adopted solutions: UUIDs have been widely adopted and proven to be - effective in various use cases, including distributed systems, databases, - and web applications. This extensive usage has resulted in a wealth of - knowledge, libraries, and best practices associated with working with UUIDs, - making them a reliable choice. - -In summary, the choice between CUID and UUID depends on the specific -requirements of the application or system. CUIDs offer simplicity, shorter -length, and collision resistance, making them suitable for scenarios where -readability and uniqueness are critical. On the other hand, UUIDs provide -universality, standardization, higher entropy, and wider adoption, making them a -strong choice for applications that require compatibility, security, and a -proven track record. - -
- -That pretty much checks out for my understanding of the debate. The primary -benefit of CUIDs for the Epic Stack is their emphasis on collision resistance -which is good for distributed systems (which is the Epic Stack). Also, it's nice -to keep things smaller and more readable. - -## Decision - -Use CUIDs for all models instead of UUIDs. - -## Consequences - -No migration is necessary since the data type for CUIDs and UUIDs is both -"string". If any existing application wants to proactively update old values to -CUIDs, that's fine, but not completely necessary. All that needs to be updated -is the prisma schema for a few of the models that were using UUIDs. Generating a -new version of the client (which happens automatically at build time) is -sufficient to handle this schema change. diff --git a/docs/decisions/013-email-code.md b/docs/decisions/013-email-code.md deleted file mode 100644 index c65d90f..0000000 --- a/docs/decisions/013-email-code.md +++ /dev/null @@ -1,40 +0,0 @@ -# Email Verification Code - -Date: 2023-06-05 - -Status: accepted - -## Context - -When a new user registers, we need to collect their email address so we can send -them a password reset link if they forget their password. Applications may also -need the email for other reasons, but whatever the case may be, we need their -email address, and to reduce spam and user error, we want to verify the email as -well. - -Currently, the Epic Stack will send the email with a link which the user can -then click and start the onboarding process. This works fine, but it often means -the user is left with a previous dead-end tab open which is kind of annoying -(especially if they are on mobile and the email client opens the link in a -different browser). - -An alternative to this is to include a verification code in the email and have -the user enter that code into the application. This is a little more work for -the user, but it's not too bad and it means that the user can continue their -work from the same tab they started in. - -This also has implications if people want to add email verification for -sensitive operations like password resets. If a code system is in place, it -becomes much easier to add that verification to the password reset process as -well. - -## Decision - -We will support both options. The email will include a code and a link, giving -the user the option between the two so they can select the one that works best -for them in the situation. - -## Consequences - -This requires a bit more work, but will ultimately be a better UX and will pave -the way for other features in the future. diff --git a/docs/decisions/014-totp.md b/docs/decisions/014-totp.md deleted file mode 100644 index 0d11027..0000000 --- a/docs/decisions/014-totp.md +++ /dev/null @@ -1,114 +0,0 @@ -# TOTP (Time-based One-time Password Algorithm) - -Date: 2023-06-06 - -Status: accepted - -## Context - -As explained in [013-email-code.md](./013-email-code.md), user registration -requires email verification. To verify the email address, we send the user an -email with a magic link that includes an encrypted payload. We do something -similar for the "forgot password" flow as well. - -Early on, it was decided to not bother creating a record with the database for -new users. This simplifies things a bit, but it also means that the salt is the -same for all users which technically opens us up for -[rainbow table](https://en.wikipedia.org/wiki/Rainbow_table) attacks. We could -also have implemented this using JWTs because the payload doesn't actually -contain any sensitive information and all we need is to verify that someone else -didn't generate the token (which is what JWTs enable). - -The real issue is that since the payload is known (our source for this is -literally open) and the salt is known (it's the same for all users), a dedicated -adversary with enough time and resources could brute-force their way to -determine the `ENCRYPTION_SECRET`... Or a disgruntled employee could leak it. - -If the `ENCRYPTION_SECRET` were to somehow determined by an adversary, it would -allow them to generate magic links for any email address and login as any user. -Even if the risk is a little low, it's still a risk that would be nice to avoid -provided it didn't impose an enormous effort. - -One way I explored for reducing this risk is rotating the encryption secret by -having a special database table for storing the secret and then having a -background job that rotates the secret every so often. This would also mean that -we need to keep old secrets around for as long as the encrypted data is valid so -we can decrypt it. We don't yet have background job support, but we're planning -on adding it eventually (we probably should do something like this for our -`INTERNAL_COMMAND_TOKEN` in the future). In any case, it would be nice to avoid -the extra complexity. - -And so we come to TOTP (Time-based One-Time Passwords). TOTP is a standard for -generating one-time passwords that are only valid for a specific amount of time. -The most common example of this is the 2FA codes that you get when logging into -a website. -[Read how TOTP works on Wikipedia](https://en.wikipedia.org/wiki/Time-based_one-time_password). -In our case it's pretty simple because we are both the client and server. - -Here's the TL;DR: - -1. Generate a secret key -2. Generate the TOTP with that key -3. Store the key and the TOTP in the database along with the thing you're - verifying (like the user's email address) -4. Send the TOTP to the user (email it to them) -5. When the user enters the TOTP, verify it against the one in the database - that's associated with the thing they're verifying -6. If the TOTP is valid, delete it from the database and allow the user to - proceed - -So yeah, it's literally a short-lived, one-time password. - -Think of it this way: Before, we generated a one-time password (the token of the -magic link) using the same secret. Now we generate a one-time password using a -random secret. It becomes impossible for someone to brute-force discovery of our -secret. - -**Why not just send the secret key instead of generating a TOTP?** First of all, -we want to make sure this is something easy for a user to type in. A 6-digit -number is much easier to type than a 32-character string. So we could make the -secret key be 6 digits long, but the other benefit of the TOTP is that it -expires after a certain amount of time by design. So brute force attacks are -much less likely to succeed. - -On top of all that, putting this together makes features like 2FA much easier to -implement because we can just reuse the same code. - -One unfortunate aspect of using a TOTP instead of an environment variable is you -now need to store the secret key in the database. Personally I really liked that -the email verification was effectively stateless, but I think the security -benefits of using a TOTP outweigh the costs. - -As for generating the TOTP, there are a few libraries for this, but every one -that I could find either did way too much or hasn't been updated in many years -and has a number of flaws and limitations. So we'll need to implement our own -based on the simplest implementation I could find: [`notp`](https://npm.im/notp) -(it doesn't support custom algorithms and uses `sha1` 😬). A perfectly good TOTP -can be generated in less than a hundred lines of Node.js code. - -A lot of this is inspired by -[this conversation with ChatGPT 4](https://chat.openai.com/share/a1bbd00d-c9d7-4846-a9af-12c6a475cd20). -It's a really good conversation and I recommend reading it. - -## Decision - -We'll change the magic link token to be an TOTP instead of an encrypted payload. - -## Consequences - -This makes it much easier to implement the email code verification feature -decided on in [013-email-code.md](./013-email-code.md). It also makes it easier -to implement 2FA which we'll do in the future. This also allows us to remove the -`ENCRIPTION_SECRET` from the list of environment variables you need to manage. -And we can swap the `encryption.server.ts` utility for a `totp.server.ts` -utility which, while not simpler is a fair trade (and could definitely be -developed and externalized into an open source library). **Update:** This has -now been published as [@epic-web/totp](https://npm.im/@epic-web/totp). - -This also means we now need a new table in the database. This can be designed in -such a way that there's no migration cost and it's purely additive. - -Eventually, we'll want to set up a background job that deletes expired TOTPs -from the database. It's not a ship stopper for this feature, but something we'll -want to have implemented eventually (the same applies to expired sessions as -well). diff --git a/docs/decisions/015-monitoring.md b/docs/decisions/015-monitoring.md deleted file mode 100644 index 545f2a5..0000000 --- a/docs/decisions/015-monitoring.md +++ /dev/null @@ -1,36 +0,0 @@ -# Monitoring - -Date: 2023-06-09 - -Status: accepted - -## Context - -Unless you want to be watching your metrics and logs 24/7 you probably want to -be notified when users experience errors in your application. There are great -tools for monitoring your application. I've used Sentry for years and it's -great. - -One of the guiding principles of the project is to avoid services. The nature of -application monitoring requires that the monitor not be part of the application. -So, we necessarily need to use a service for monitoring. - -One nice thing about Sentry is it is open source so we can run it ourselves if -we like. However, that may be more work than we want to take on at first. - -## Decision - -We'll set up the Epic Stack to use Sentry and document how you could get it -running yourself if you prefer to self-host it. - -We'll also ensure that we defer the setup requirement to later so you can still -get started with the Epic Stack without monitoring in place which is very useful -for experiments and makes it easier to remove or adapt to a different solution -if you so desire. - -## Consequences - -We tie the Epic Stack to Sentry a bit, but I think that's a solid trade-off for -the benefit of production error monitoring that Sentry provides. People who need -the scale where Sentry starts to cost money (https://sentry.io/pricing/) will -probably be making money at that point and will be grateful for the monitoring. diff --git a/docs/decisions/016-source-maps.md b/docs/decisions/016-source-maps.md deleted file mode 100644 index d7c2de3..0000000 --- a/docs/decisions/016-source-maps.md +++ /dev/null @@ -1,70 +0,0 @@ -# Source Maps - -Date: 2023-06-14 - -Status: superseded by [034-source-maps](034-source-maps.md) - -## Context - -If you're unfamiliar with source maps, check out -[What are source maps?](https://web.dev/source-maps/) on web.dev. - -For anyone familiar with source maps, it's pretty obvious that you do want these -in production for debugging purposes (read -[Should I Use Source Maps in Production? ](https://css-tricks.com/should-i-use-source-maps-in-production/)). -However, when you enable source maps with Remix, you get a warning that looks -like this: - -``` -> remix build --sourcemap - -Building Remix app in production mode... - -⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ -You have enabled source maps in production. This will make your server-side code visible to the public and is highly discouraged! If you insist, please ensure you are using environment variables for secrets and not hard-coding them into your source! -⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ -``` - -It's pretty well understood that source maps allow your code to be visible to -the public and yet it's still pretty well understood that's fine because -client-side apps send the code anyway and that can typically be reverse -engineered (especially with the help of modern AI tools as well). - -The reason it's a special concern for some frameworks like Remix today is -because the source maps for the client-side code include some of the server-side -code as well. This is because Remix and other frameworks like it have the -ability for you to write your server code in the same file as your browser code. - -The ability for the public to view your server-side code understandably raises -some concerns for folks. This is especially dangerous if you have secrets -hard-coded into your server-side code. Additionally, if you're exercising -"security through obscurity" then you may be vulnerable to attacks if your -server-side code is visible. - -On the flip side, you really shouldn't be hard-coding secrets into your -server-side code anyway. You should be using environment variables for that. -Additionally, if you're relying on "security through obscurity" then you're -probably not as secure as you think you are. - -Also, source maps are necessary for error monitoring with tools like Sentry. -Without source maps, you'll only see the minified code in your error monitoring -tools. This makes it much harder to debug errors in production. And if you're -debugging your application in production you'll also be limited to minified code -as well. - -It may be possible to generate the source maps and make them available to -Sentry, but then prevent them from being sent to the client. More research is -needed to determine whether this is possible. - -## Decision - -We've decided to enable source maps in production by default. This will allow -for better error monitoring and debugging in production. It will also allow for -easier debugging of server-side code in production. - -## Consequences - -Developers using the Epic Stack will see an warning message during the build and -if they don't practice good secret "hygiene" they may be vulnerable to attacks. -So we'll add documentation explaining how to properly use environment variables -for secrets and not hard-code them into your source code. diff --git a/docs/decisions/017-resend-email.md b/docs/decisions/017-resend-email.md deleted file mode 100644 index a52f7c8..0000000 --- a/docs/decisions/017-resend-email.md +++ /dev/null @@ -1,29 +0,0 @@ -# Migrating to Resend - -Date: 2023-06-20 - -Status: accepted - -## Context - -Mailgun changed their pricing model to make it more difficult to understand what -is available within the free tier which motivated us to re-evaluate our -selection here. While mailgun is still a fine service, -[Resend](https://resend.com/) has caught the attention of several users of the -Epic Stack. It has a generous (and obvious) free tier of 3k emails a month. They -check all the boxes regarding table-stakes features you'd expect from an email -service. On top of those things, the UI is simple and easy to use. It's also a -lot cheaper than Mailgun. - -## Decision - -We'll migrate to Resend. As a part of this migration, we're going to avoid -coupling ourselves too closely to it to make it easier to switch to another -provider if you so desire. So we'll be using the REST API instead of the SDK. - -## Consequences - -Code changes are relatively minimal. Only the `app/utils/email.server.ts` util -and the mock for it need to be changed. Then we also need to update -documentation to use the Resend API key instead of the mailgun sending domain, -etc. diff --git a/docs/decisions/018-images.md b/docs/decisions/018-images.md deleted file mode 100644 index c94b743..0000000 --- a/docs/decisions/018-images.md +++ /dev/null @@ -1,61 +0,0 @@ -# Images - -Date: 2023-06-23 - -Status: accepted (for now) - -## Context - -One of the guiding principles of the Epic Stack is to limit services (including -the self-managed variety). Depending on the needs of your application, you may -be justified in reaching for a service to solve this problem. If you have many -large images, then finding a service that can host them for you makes a lot of -sense. - -Currently, the Epic Stack stores images in the SQLite database as a blob of -bytes. At first glance, you may think this is a really bad idea, and for some -use cases it definitely would be. But it scales surprisingly well (in some -cases, -[serving small files from SQLite can be faster than the file system](https://www.sqlite.org/fasterthanfs.html)). -In fact, thanks to LiteFS, you get the benefits of replicated storage to all -your app nodes. - -Currently, the setup is pretty sub-optimal. There's currently no optimization or -compression of these images. Whatever goes in is what comes out regardless of -needs of the client requesting the image. And if you plan on handling a lot of -images, you could bump against the limits of SQLite + LiteFS (it's been tested -up to 10GBs). - -These limits should be fine for a large number of applications, but we don't -want "fine" we want Epic! - -Another guiding principle of the Epic Stack is to make things adaptable. We -haven't really come around to this for images yet, but hopefully in the future -there will be a good solution to making it easy to swap from the self-hosted -images to a service. - -We also have plans to support automatic optimization of images a la -Cloudinary/Cloudflare. - -One thing we're waiting on is -[LiteFS to support object storage](https://github.com/superfly/litefs/issues/327). -Once that's done, then we'll probably move the images to files in your volume -and we'll also be able to use that to cache optimized versions of the images. -This will have limited scale, but should be Epic for many applications. - -But all of this is work that hasn't been done yet, so if you're adopting the -Epic Stack, you may consider adjusting the image to use a service. And if you've -got big plans for images in your site, you may want to consider a service. - -## Decision - -We'll leave things as they are for now mostly due to time constraints. Examples -of using the Epic Stack with services are encouraged and welcome. We definitely -want to make it easy to swap out the self-hosted images for a service, so help -there would be appreciated as well. - -## Consequences - -People may start off projects that have ambitious image needs without realizing -the image setup here will not satisfy their requirements. A migration would be -annoying, but definitely possible. diff --git a/docs/decisions/019-components.md b/docs/decisions/019-components.md deleted file mode 100644 index fb0bbfd..0000000 --- a/docs/decisions/019-components.md +++ /dev/null @@ -1,80 +0,0 @@ -# Components - -Date: 2023-06-27 - -Status: accepted - -## Context - -The web platform is severely lacking in terms of UI components. There's pretty -minimal by way of built-in components, and for many that do exist, they are -extremely difficult (if not impossible) to style. - -I have yet to build a web application where the product owner was happy with the -user agent styles and capabilities of components that are built into the web -platform. - -Unfortunately, building components that work nicely with assistive technologies -in any way you would consider "accessible" is non-trivial. It's certainly not -something that you'd want to do in every new project. - -So using a library that provides accessible components users expect is -definitely a good idea. However, many component libraries are difficult to -customize from a styling perspective. - -What's best is to get a "headless" UI library: One which handles the logic of -accessible, reusable components, but leaves the styling up to you. - -While it may make sense to just say "let's use web components" I'm going to -leave the argument against web components to -[Rich Harris](https://dev.to/richharris/why-i-don-t-use-web-components-2cia) -(he's right, and it pains me just like it does him). The Epic Stack comes with -React, so a component library that leans on React is no problem and actually a -nice benefit. - -Having been around the block a few times myself (and even having built a handful -of component libraries), I've determined the library that does this best is -[Radix](https://www.radix-ui.com/). It's a terrific collection of primitive -components that has a fantastically composable API. - -The Epic Stack started with Radix from the start for this reason. - -That leaves us with the decision about how to style things. The Epic Stack -started with Tailwind for styling (no decision document has been written about -this choice yet), and shipped with things styled by Tailwind. It has worked -relatively well, but the structure has been challenging for folks adopting the -Epic Stack. It's left adopters of the Epic Stack with a lot challenges around -customization. - -Customization is always the biggest challenge when it comes to styling -components. Every company wants its own take on the UI, so having a component -library that comes with its styles baked in is a non-starter. This is why we -chose a headless component library in the first place. - -This is where [shadcn/ui](https://ui.shadcn.com/) comes into the picture. It's -not a component library, but more of a code registry where you can -copy/paste/modify the code to your heart's content. Additionally, it comes -opinionated with our own opinions! It's built with Tailwind and Radix. - -Additionally, while you can easily copy/paste/modify from the website, you can -also use the CLI to download components as needed. So we can add a configuration -file to the Epic Stack and the CLI will know exactly where to place files. - -On top of that, shadcn/ui assumes a Tailwind setup that relies heavily on CSS -variables for color styles which makes it much easier to adapt to the light/dark -mode theme of the Epic Stack. - -## Decision - -We'll adopt shadcn/ui, Radix, and Tailwind as the UI component solution for the -Epic Stack. We'll move most of the custom components that are currently in the -Epic Stack to shadcn/ui components. We'll customize those components as needed. - -## Consequences - -It's important to keep in mind that because shadcn/ui is not a component -library, updates for these components are similar to updates in the Epic Stack -itself: manual. There is no way to get automated updates here. And this is -actually a good thing, even though it's a bit more work. It's a good thing -because it means that you can customize the components as much as you want -without worrying about breaking changes. diff --git a/docs/decisions/020-icons.md b/docs/decisions/020-icons.md deleted file mode 100644 index d32a8dc..0000000 --- a/docs/decisions/020-icons.md +++ /dev/null @@ -1,42 +0,0 @@ -# Icons - -Date: 2023-06-28 - -Status: accepted - -## Context - -Icons are a critical part to every application. It helps users quickly identify -different actions they can take and the meaning of different elements on the -page. It's pretty well accepted that SVGs are the way to go with icons, but -there are a few different options for how to go about doing this. - -Because the Epic Stack is using React, it may feel obvious to just use a -component per icon and inline the SVG in the component. This is fine, but it's -sub-optimal. I'm not going to spend time explaining why, because -[this article does a great job of that](https://benadam.me/thoughts/react-svg-sprites/). - -SVG sprites are no less ergonomic than inline SVGs in React because in either -case you need to do some sort of transformation of the SVG to make it useable in -the application. If you inline SVGs, you have [SVGR](https://react-svgr.com/) to -automate this process. So if we can automate the process of creating and -consuming a sprite, we're in a fine place. - -And [rmx-cli](https://github.com/kiliman/rmx-cli) has support for automating the -creation of an SVG sprite. - -One drawback to sprites is you don't typically install a library of icons and -then use them like regular components. You do need to have a process for adding -these to the sprite. And you wouldn't want to add every possible icon as there's -no "tree-shaking" for sprites. - -## Decision - -Setup the project to use SVG sprites with `rmx-cli`. - -## Consequences - -We'll need to document the process of adding SVGs. It's still possible to simply -install a library of icons and use them as components if you're ok with the -trade-offs of that approach. But the default in the starter will be to use -sprites. diff --git a/docs/decisions/021-node-version.md b/docs/decisions/021-node-version.md deleted file mode 100644 index 8406358..0000000 --- a/docs/decisions/021-node-version.md +++ /dev/null @@ -1,58 +0,0 @@ -# Node.js LTS - -Date: 2023-07-03 - -Status: accepted - -## Context - -Node.js has a regular release cycle which is documented in the -[release schedule](https://nodejs.org/en/about/releases/). At the time of this -writing, there are 3 stable maintained releases: 16, 18, and 20. I'll refer you -to that documentation to understand how the release cycle works. - -Deciding which version of Node.js to use for a project is a trade-off between -using the latest features and stability. - -The Epic Stack is more focused on stably shipping web apps than experimenting -with the latest features which is where the Active Long-Term Support (LTS) -version really shines. - -We deploy our apps in Docker containers, and there are various base images we -can use as options which you can find on -[the Node.js Docker Hub](https://hub.docker.com/_/node). Aside from the version, -there flavors of the base image which are based on the Linux distribution used. -Feel free to read more about the different flavors on Docker Hub. One of the -goals for us here is to not ship more than we need in production. - -An additional consideration we'll add as context here is what version of Linux -to have our base image built on. With the same pragmatic approach as the Node.js -version we want to balance latest features with stability. We'll use the -[Debian release cycle](https://wiki.debian.org/DebianReleases) as a guide for -this. - -## Decision - -Use the current LTS version of Node.js as the default in the starter. - -We'll use the `slim` flavor of the node.js images. - -We'll use the `bookworm` flavor of the node.js images (which is the current -stable version of Debian: v12). - -## Consequences - -Folks should hopefully run into few compatibility issues. It's possible they -will need features that are not back-ported to the current active LTS version, -however it's trivial to update the Node.js version. Added documentation to the -[managing updates](#app/managing-updates.md) docs should help people manage -this. - -We'll need to update the Node.js version in the starter whenever the active LTS -version changes. - -Folks who need a bunch more out of their operating system packages will need to -switch from the `slim` flavor which only involves updating the `Dockerfile`. It -is possible some will not realize they need more than `slim` until they run the -Docker image (which many people will only do in production). However the -likelihood of this impacting anyone is pretty low. diff --git a/docs/decisions/022-report-only-csp.md b/docs/decisions/022-report-only-csp.md deleted file mode 100644 index 4a7b7c5..0000000 --- a/docs/decisions/022-report-only-csp.md +++ /dev/null @@ -1,31 +0,0 @@ -# Report-only CSP - -Date: 2023-07-14 - -Status: accepted - -## Context - -The Epic Stack uses a strict -[Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP). -All the reasons for this explained in -[the decision document](./008-content-security-policy.md) still apply. However, -As people adapt the Epic Stack to their own needs, they may easily forget to add -important sources to the CSP. This can lead to a frustrating experience for new -users of the Epic Stack. - -There's an option for CSPs called `report-only` which allows the browser to -report CSP violations without actually blocking the resource. This turns the CSP -into an opt-in which follows our [guiding principle](#app/guiding-principles.md) -of "Minimize Setup Friction" (similar to deferring setup of third-party services -until they're actually needed). - -## Decision - -Enable report-only on the CSP by default. - -## Consequences - -New users of the Epic Stack won't be blocked by the CSP by default. But this -also means they won't be as safe by default. We'll need to make sure enforcing -the CSP is documented well. diff --git a/docs/decisions/023-route-based-dialogs.md b/docs/decisions/023-route-based-dialogs.md deleted file mode 100644 index 4fd92eb..0000000 --- a/docs/decisions/023-route-based-dialogs.md +++ /dev/null @@ -1,45 +0,0 @@ -# Route-based Dialogs (aka Modals) - -Date: 2023-07-14 - -Status: accepted - -## Context - -Dialogs (also known as modals) are often a crutch for poor UX design. They are -often used when you haven't thought through the design of the page within the -context of the user's intentions. - -They aren't always bad though. Sometimes they are useful to provide a -confirmation step before a destructive action. For this we already have the -`useDoubleCheck` hook which makes it easier to help the user confirm their -action, but using a dialog gives you the opportunity to explain to the user a -bit more before the action is completed. - -However, using Dialogs for routes is problematic. Dialogs without animations are -poor UX. But server rendering animations is problematic because it means the -user has to wait for the animation code to load before they see the content they -came for. - -Unsplash solves this problem by using dialogs for images when you click on them, -but when you refresh the page you see that image's page. This is an intentional -decision by them and I'm sure they weighed the pros and cons for this UX. -However, it's not often this is a good user experience. - -Until today, the Epic Stack used route-based dialogs for the 2FA flow and the -avatar edit experience. I like using routes for these so it's easy to link the -user directly to these pages and makes it easier to navigate in and out of them. - -These are definitely not a good use of route-based dialogs. It certainly doesn't -make sense to render it as a dialog for a client-navigation but something else -for landing on that page like unsplash does for its images. - -## Decision - -Remove route-based dialogs from the Epic Stack. - -## Consequences - -A better UX. What used to be dialogs will now simply be pages. To help with -navigation, we'll need to use breadcrumbs to help the user orient themselves and -find a way back to where they came from. diff --git a/docs/decisions/024-change-email.md b/docs/decisions/024-change-email.md deleted file mode 100644 index e084ed9..0000000 --- a/docs/decisions/024-change-email.md +++ /dev/null @@ -1,68 +0,0 @@ -# Change Email - -Date: 2023-07-26 - -Status: accepted - -## Context - -For most websites, the user's email is the primary mechanism for authenticity. -You sign up with your email, password reset involves the email, and often you -sign in with your email. - -For various reasons, users may wish to maintain their account data, but change -their email address. There are a few important considerations for managing this: - -1. The user may no longer have access to the current email address. -1. If the user typos their email address, then they may lose access to their - account. -1. If an adversary gets temporary access to a user's account, they may be able - to change the victim's email address to one they own. - -There are a few ways to address these concerns. Here are a few things you could -do (some in combination) - -- Let the user change the email with no validation -- Notify the new email address of the change -- Notify the old email address of the change -- Require confirmation of the new address before allowing the change -- Require confirmation of the old address before allowing the change -- Require a two-factor code before allowing the change - -The ultimate secure approach would be: - -- Require a two-factor code and confirmation from the old and new address before - allowing the change. - -This has a few problems: - -1. Not all users have 2FA enabled -2. Users don't always have access to their old address - -If you really needed that level of security, you could require 2FA and users -could reach out to support if they don't have access to the old email to plead -their case. - -However, there's a middle-ground: - -- Require a two-factor code from users who have it enabled, receive confirmation - of the new address, and notify the old address. - -This strikes a good balance of being easy for the user, reducing the number of -support requests, and security. - -## Decision - -We're going to require recent (within the last 2 hours) verification of the -two-factor code if the user has it enabled, require confirmation of the new -address, and notify the old address of the change. - -## Consequences - -This will require supporting a mechanism for tracking when the last 2FA code was -entered (just storing the time in the session). This will also require a new -verification for confirming the new address (utilizing existing verification -utilities we have for onboarding the user in the first place). - -It's a little bit more complicated than just letting the user change their email -address, but will hopefully reduce the problems described. diff --git a/docs/decisions/025-rate-limiting.md b/docs/decisions/025-rate-limiting.md deleted file mode 100644 index 4419cbf..0000000 --- a/docs/decisions/025-rate-limiting.md +++ /dev/null @@ -1,62 +0,0 @@ -# Rate Limiting - -Date: 2023-08-10 - -Status: accepted - -## Context - -Adversaries can sometimes attempt to break into user's accounts by guessing -their passwords. This is known as a brute force attack. - -Or, sometimes a bad guy may not like you and want to do you harm so they will -trigger you to send people a lot of emails by hitting your `/signup` or -`/settings/profile/change-email` endpoint over and over again. Doing this will -reduce your reputation with email providers and may cause your emails to be -flagged as spam. - -A common way to reduce the impact and likelihood of this is to rate limit -requests. This means that you only allow a certain number of requests from a -given IP address within a certain time period. - -There are established patterns and libraries for doing this. The most popular -and well maintained library for express is -[express-rate-limit](https://npm.im/express-rate-limit). - -One challenge with rate limiting in a production environment is if you have -multiple instances of your application running behind a load balancer (which in -our case is Fly). In this case, you need to ensure that the rate limit is -applied across all instances and not just to each individual instance. -`express-rate-limit` allows you to do this by using a shared storage mechanism. -A common solution is Redis or memcached. - -Rate limiting doesn't completely eliminate the problem of triggering unsolicited -emails (CSRF tokens will do an even better job of reducing those) but it does -help a great deal. - -Another thing to consider is what the rate limit levels will be. The entire -application should not necessarily have the same rate limit. Users of web -applications will often perform many more GET requests than they do POST -requests for example. So some endpoints and methods will require "stronger" rate -limits than others. - -## Decision - -We will use `express-rate-limit` to rate limit requests to our application. We -will also use the built-in memory storage mechanism as the default. This is good -enough for many applications and is the simplest to implement. Evolving to a -Redis based solution should not require a great deal of extra effort for folks -requiring that additional level of protection. - -We'll have a stronger rate limit on non-GET requests in general and an even -stronger rate limit on certain endpoints that are more likely to be abused. - -## Consequences - -This could mean that folks who are using the application from a shared IP -address (such as a corporate network) may be rate limited more aggressively than -we would like. This is a trade-off we are willing to make for now. - -Our default levels for rate limiting could also be overly aggressive for some -people's use cases causing confusion. So we'll need to document this to help -people be made aware of the potential issue and how to resolve it. diff --git a/docs/decisions/026-path-aliases.md b/docs/decisions/026-path-aliases.md deleted file mode 100644 index 5d11cf3..0000000 --- a/docs/decisions/026-path-aliases.md +++ /dev/null @@ -1,36 +0,0 @@ -# Path Aliases - -Date: 2023-08-14 - -Status: superseded by [031-imports](./031-imports.md) - -## Context - -It's pretty common to configure TypeScript to have path aliases for imports. -This allows you to avoid relative imports and makes it easier to move files -around without having to update imports. - -When the Epic Stack started, we used path imports that were similar to those in -the rest of the Remix ecosystem: `#` referenced the `app/` directory. We added -`tests/` to make it easier to import test utils. - -However, we've found that this is confusing for new developers. It's not clear -what `#` means, and seeing `import { thing } from 'tests/thing'` is confusing. I -floated the idea of adding another alias for `@/` to be the app directory and or -possibly just moving the `#` to the root and having that be the only alias. But -at the end of the day, we're using TypeScript which will prevent us from making -mistakes and modern editors will automatically handle imports for you anyway. - -At first it may feel like a pain, but less tooling magic is better and editors -can really help reduce the pain. Additionally, we have ESLint configured to sort -imports for us so we don't have to worry about that either. Just let the editor -update the imports and let ESLint sort them. - -## Decision - -Remove the path aliases from the `tsconfig`. - -## Consequences - -This requires updating all the imports that utilized the path aliases to use -relative imports. diff --git a/docs/decisions/027-toasts.md b/docs/decisions/027-toasts.md deleted file mode 100644 index 3474456..0000000 --- a/docs/decisions/027-toasts.md +++ /dev/null @@ -1,36 +0,0 @@ -# Toasts - -Date: 2023-08-14 - -Status: accepted - -## Context - -In the Epic Stack we used the Shadcn toast implementation. This worked ok, but -it did require a lot of custom code for ourselves and did a poor job of managing -multiple toast messages. - -We also had a shared `flash` session implementation for both toasts and -confetti. This was overly complex. - -There's another library -[someone told me about](https://twitter.com/ayushverma1194/status/1674848096155467788) -that is a better fit. It's simpler and has an API sufficient to our use cases. - -It's also sufficiently customizable from a design perspective as well. And it's -actively developed. - -## Decision - -Remove our own toast implementation and use the library instead. - -Also separate the toast and confetti session implementations. Toasts can -continue to use a regular session, but confetti will be a much simpler cookie. - -## Consequences - -This will limit the level of customizability because we're now relying on a -library for managing toast messages, however it also reduces the maintenance -burden for users of the Epic Stack. - -This will also simplify the confetti implementation. diff --git a/docs/decisions/028-permissions-rbac.md b/docs/decisions/028-permissions-rbac.md deleted file mode 100644 index 538ad72..0000000 --- a/docs/decisions/028-permissions-rbac.md +++ /dev/null @@ -1,84 +0,0 @@ -# Permissions (RBAC) - -Date: 2023-08-14 - -Status: accepted - -## Context - -Originally, the Epic Stack had a `role` and `permission` model which was quite -limited in its use case. It was not very useful and not based on any real world -scenario: - -```prisma -model Role { - id String @id @unique @default(cuid()) - name String @unique - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - users User[] - permissions Permission[] -} - -model Permission { - id String @id @unique @default(cuid()) - name String @unique - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - roles Role[] -} -``` - -There are various ways to implement permissions, but a common approach is called -[Role Based Access Control (RBAC)](https://auth0.com/intro-to-iam/what-is-role-based-access-control-rbac). -This is a very flexible approach and can be used in many different ways. As a -more established approach it's also easier to find resources to learn about and -understand it. - -## Decision - -We're changing the implementation to follow a RBAC model: - -```prisma -model Permission { - id String @id @default(cuid()) - action String // e.g. create, read, update, delete - entity String // e.g. note, user, etc. - access String // e.g. own or any - description String @default("") - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - roles Role[] - - @@unique([action, entity, access]) -} - -model Role { - id String @id @default(cuid()) - name String @unique - description String @default("") - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - users User[] - permissions Permission[] -} -``` - -This allows us to have much more fine grained control over our permissions. -Additionally, we can create utilities for determining whether a user has -permission to perform an action and disallow them from doing so if they do not. - -## Consequences - -This is a breaking change for the Epic Stack. Anyone wanting to adopt this -permissions model will need to perform a database migration. However, it's -important that we make this change now because the previous model was not great. -This one is. diff --git a/docs/decisions/029-remix-auth.md b/docs/decisions/029-remix-auth.md deleted file mode 100644 index 86e5926..0000000 --- a/docs/decisions/029-remix-auth.md +++ /dev/null @@ -1,22 +0,0 @@ -# Remix Auth - -Date: 2023-08-14 - -Status: accepted - -## Context - -At the start of Epic Stack, we were using -[remix-auth-form](https://github.com/sergiodxa/remix-auth-form) for our -username/password auth solution. This worked fine, but it really didn't give us -any value over handling the auth song-and-dance ourselves. - -## Decision - -Instead of relying on remix-auth for handling authenticating the user's login -form submission, we'll manage it ourselves. - -## Consequences - -This mostly allows us to remove some code. However, we're going to be keeping -remix auth around for GitHub Auth diff --git a/docs/decisions/030-github-auth.md b/docs/decisions/030-github-auth.md deleted file mode 100644 index 6eca20a..0000000 --- a/docs/decisions/030-github-auth.md +++ /dev/null @@ -1,69 +0,0 @@ -# GitHub Auth - -Date: 2023-08-14 - -Status: accepted - -## Context - -Many applications require integrating with third party authentication providers. -For this reason, we want to support the idea of "connections" as a built-in part -of the Epic Stack. - -There are many different providers we could support, but many people need to -support more than just one. By building things in a way that allows us to -support more than just a single auth provider, it allows us to also make it easy -to swap to a different provider as needed. - -Many auth providers support OAuth2, but increasingly, many are also supporting -OpenID Connect. OpenID Connect is a layer on top of OAuth2 that provides a -standardized way to get user information from the auth provider. - -Sadly, GitHub (a common popular auth provider for many developer-focused apps) -does not support OpenID, however, by using -[`remix-auth`](https://github.com/sergiodxa/remix-auth), we can easily support -GitHub as a built-in implementation and allow people to swap it out for whatever -OAuth2 or OIDC auth provider they have (if OIDC, they can use -[web-oidc](https://github.com/sergiodxa/web-oidc)). - -## Decision - -We will update the database schema to support multiple auth providers with a -model called `Connection`: - -```prisma -model Connection { - id String @id @default(cuid()) - providerName String - providerId String @unique - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade) - userId String - - @@unique([providerName, providerId]) - @@unique([providerId, userId]) -} -``` - -We'll also build the appropriate callback URL handlers and UI to allow people to -manage their connections. - -## Consequences - -With third party auth, this means that users may not have passwords. So we'll -need to handle that situation and allow users to onboard without the use of -passwords. We'll also need to prevent them from deleting all their connections -until they've created a password. - -There are a number of states for the user to be in within the callback as well -which all will need to be considered. All of these states will be tested to -ensure they continue to function properly as people tune things for their needs. - -Additionally, we'll need to account for the fact that some folks don't want to -set up the GitHub login flow from the start (to keep in line with our -[Minimize Setup Friction guiding principle](#app/guiding-principles.md)), so -we'll have to make sure that the app still runs properly without GitHub auth -configured. diff --git a/docs/decisions/031-imports.md b/docs/decisions/031-imports.md deleted file mode 100644 index ebd8f47..0000000 --- a/docs/decisions/031-imports.md +++ /dev/null @@ -1,76 +0,0 @@ -# Imports - -Date: 2023-08-16 - -Status: accepted - -## Context - -Recently, we removed the `~/*` and `tests/*` aliases in favor of relative -imports. The arguments for this are described in -[026-path-aliases](./026-path-aliases.md). While the arguments are sound, the -big challenge with this approach is the fact that there are some times where you -need to type out the import for something and doing that is a huge pain with -relative routes. - -The issue is the fact that you can choose one of these options: - -1. Very flat files -2. Long relative imports -3. Path aliases - -Keeping files flat is just not a great option because it requires exceedingly -long filenames for longer routes and it makes it hard to find files. Long -relative imports are just a pain to type out and they are hard to read, copy, -and manually modify. - -Despite the magic of Path aliases, they are actually a standard `package.json` -supported feature. Sort of. -[The `"imports"` field](https://nodejs.org/api/packages.html#imports) in -`package.json` allows you to configure aliases for your imports. It's not -exactly the same as TypeScript Path aliases, and using them doesn't give you -autocomplete with TypeScript -([yet](https://github.com/microsoft/TypeScript/pull/55015)), but if you -configure both, then you can get the best of both worlds! - -By using the `"imports"` field, you don't have to do any special configuration -for `vitest` or `eslint` to be able to resolve imports. They just resolve them -using the standard. - -And by using the `tsconfig.json` `paths` field configured in the same way as the -`"imports"` field, you get autocomplete and type checking for your imports. This -should hopefully be temporary until TypeScript supports the `"imports"` field -directly. - -One interesting requirement for `imports` is that they _must_ start with the `#` -character to disambiguate from other imports. This is a bit annoying, but it's -something that's not difficult to get used to. They also _must not_ start with -`#/`. So you have to do `#app` instead of `#/app`. This is also a bit odd, but -again it's just a matter of familiarity. So it's no big deal. - -## Decision - -We're going to configure `"imports"` in the `package.json` and `paths` in the -`tsconfig.json` to use path aliases for imports. - -We'll set it to `"#*": "./*"` which will allow us to import anything in the root -of the repo with `#/`. - -## Consequences - -This is unfortunately _very_ soon after making the decision to drop the alias. -But I see this as slightly different because we're only using the alias to make -up for a shortcoming in TypeScript temporarily. Once TypeScript supports the -`"imports"` field, we can drop the `paths` field and just use the `"imports"` -standard for Node.js. - -If someone wants to use the Epic Stack without Node.js, and their runtime -doesn't support `package.json` imports (I'm not sure whether other runtimes do -or not) they'll have to continue using the paths configuration. But that's not a -consideration here. - -## Credits - -Big thank you to -[Mateusz BurzyΕ„ski](https://twitter.com/AndaristRake/status/1691807097078317287) -for helping with this one! diff --git a/docs/decisions/032-csrf.md b/docs/decisions/032-csrf.md deleted file mode 100644 index 182f767..0000000 --- a/docs/decisions/032-csrf.md +++ /dev/null @@ -1,33 +0,0 @@ -# Cross-Site Request Forgery - -Date: 2023-10-11 - -Status: superseded by [035](./035-remove-csrf.md) - -## Context - -You can learn all about Cross-Site Request Forgery from -[EpicWeb.dev's forms workshop](https://forms.epicweb.dev/07). The TL;DR idea is -that a malicious adversary can trick a user into making a request to your server -that they did not intend to make. This can be used to make requests to your -server that can do anything that the user can do. - -To defend against this attack, we need to ensure that the request is coming from -a page that we control. We do this by adding a CSRF token to the page and -checking that the token is present in the request. The token is generated by our -own server and stored in an HTTP-only cookie. This means that it can't be -accessed by third parties, but it will be sent with every request to our server. -We also send that same token within the form submission and then check that the -token in the form matches the token in the cookie. - -Once set up, this is a fairly straightforward thing to do and there are great -tools to help us do it (`remix-utils` specifically). - -## Decision - -We'll implement CSRF protection to all our authenticated forms. - -## Consequences - -This is a tiny bit invasive to the code, but it doesn't add much complexity. -It's certainly worth the added security. diff --git a/docs/decisions/033-honeypot.md b/docs/decisions/033-honeypot.md deleted file mode 100644 index d906e50..0000000 --- a/docs/decisions/033-honeypot.md +++ /dev/null @@ -1,35 +0,0 @@ -# Honeypot Fields - -Date: 2023-10-11 - -Status: accepted - -## Context - -You can learn all about Honeypot Fields from -[EpicWeb.dev's forms workshop](https://forms.epicweb.dev/06). The TL;DR idea is -spam bots go around the internet filling in forms all over the place in hopes of -getting their spammy links on your site among other things. This causes extra -load on your server and in some cases can cause you issues. For example, our -onboarding process sends an email to the user. If a spam bot fills out the form -with a random email address, we'll send an email to that address and cause -confusion in the best case or get marked as spam in the worst case. - -Most of these spam bots are not very sophisticated and will fill in every field -on the form (even if those fields are visually hidden). We can use this to our -advantage by adding a field to the form that is visually hidden and then -checking that it is empty when the form is submitted. If it is not empty, we -know that the form was filled out by a spam bot and we can ignore it. - -There are great tools to help us accomplish this (`remix-utils` specifically). - -## Decision - -We'll implement Honeypot Fields to all our public-facing forms. Authenticated -forms won't need this because they're not accessible to spam bots anyway. - -## Consequences - -This is a tiny bit invasive to the code, but it doesn't add much complexity. -It's certainly worth the added benefits to our server (and email -deliverability). diff --git a/docs/decisions/034-source-maps.md b/docs/decisions/034-source-maps.md deleted file mode 100644 index f60b73b..0000000 --- a/docs/decisions/034-source-maps.md +++ /dev/null @@ -1,24 +0,0 @@ -# Source Maps - -Date: 2023-11-03 - -Status: accepted - -## Context - -Read [016-source-maps](016-source-maps.md) to come up to speed on the context. - -Because of our built-in sentry support, we need to generate source maps, but we -do not necessarily need to ship source maps to the client. Despite the arguments -made in the original source map decision document, the benefit of shipping -source maps over not shipping them is reduced thanks to Sentry. And the dangers -are still present. - -## Decision - -Delete source maps after they've been uploaded to Sentry. - -## Consequences - -This will mean debugging a production application in the client will be really -hard, but with Sentry properly configured it should definitely not be a problem. diff --git a/docs/decisions/035-remove-csrf.md b/docs/decisions/035-remove-csrf.md deleted file mode 100644 index 854c81d..0000000 --- a/docs/decisions/035-remove-csrf.md +++ /dev/null @@ -1,43 +0,0 @@ -# Remove CSRF - -Date: 2024-01-29 - -Status: accepted - -## Context - -Read more about the original CSRF decision in [032-csrf.md](./032-csrf.md). - -Modern browser support for `SameSite: Lax` and our use of that for all cookies -means that cookies are not sent on cross-site requests. This means that CSRF -protection is not needed for our cookies. - -There are however a few exceptions which motivated the original inclusion of -CSRF: - -- GET requests are not protected by `SameSite: Lax` and so are vulnerable to - CSRF attacks. However, we do not have any GET endpoints that perform mutations - on the server. The only GET endpoints we have are for fetching data and so - there is no meaningful CSRF attack that could be performed. -- The `POST /login` endpoint does not require cookies at all and so is - technically vulnerable to CSRF attacks. But anyone who could exploit this - endpoint would have to know the user's username and password anyway in which - case they could just log in as the user directly. - -With the addition of the honeypot field to prevent bots from submitting the -login form, the lack of vulnerability due to the cookie configuration, and the -fact that CSRF adds a bit of complexity to the code, it just doesn't seem worth -it to keep CSRF tokens around. - -## Decision - -Remove CSRF tokens from the codebase. - -## Consequences - -If someone adds a GET request which does mutate state, then this could be an -issue. However, a CSRF token could be added back for that specific mutation. -Also, if the cookie configuration is changed from `Lax` to `None` (useful in -various contexts, but certainly not a good default), then CSRF tokens would need -to be added back. So we'll add a comment to the code for configuring the cookie -mentioning this. diff --git a/docs/decisions/README.md b/docs/decisions/README.md deleted file mode 100644 index 72d0909..0000000 --- a/docs/decisions/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# Decisions - -This directory contains all the decisions we've made for this starter template -and serves as a record for whenever we wonder why certain decisions were made. - -Decisions in here are never final. But these documents should serve as a good -way for someone to come up to speed on why certain decisions were made. diff --git a/docs/deployment.md b/docs/deployment.md deleted file mode 100644 index 5ed13d4..0000000 --- a/docs/deployment.md +++ /dev/null @@ -1,143 +0,0 @@ -# Deployment - -When you first create an Epic Stack repo, it should take you through a series of -questions to get your app setup and deployed. However, we'll document the steps -here in case things don't go well for you or you decide to do it manually later. -Here they are! - -## Deploying to Fly.io - -Prior to your first deployment, you'll need to do a few things: - -1. [Install Fly](https://fly.io/docs/getting-started/installing-flyctl/). - - > **Note**: Try `flyctl` instead of `fly` if the commands below won't work. - -2. Sign up and log in to Fly: - - ```sh - fly auth signup - ``` - - > **Note**: If you have more than one Fly account, ensure that you are signed - > into the same account in the Fly CLI as you are in the browser. In your - > terminal, run `fly auth whoami` and ensure the email matches the Fly - > account signed into the browser. - -3. Create two apps on Fly, one for staging and one for production: - - ```sh - fly apps create [YOUR_APP_NAME] - fly apps create [YOUR_APP_NAME]-staging - ``` - - > **Note**: Make sure this name matches the `app` set in your `fly.toml` - > file. Otherwise, you will not be able to deploy. - -4. Initialize Git. - - ```sh - git init - ``` - -- Create a new [GitHub Repository](https://repo.new), and then add it as the - remote for your project. **Do not push your app yet!** - - ```sh - git remote add origin - ``` - -5. Add secrets: - -- Add a `FLY_API_TOKEN` to your GitHub repo. To do this, go to your user - settings on Fly and create a new - [token](https://web.fly.io/user/personal_access_tokens/new), then add it to - [your repo secrets](https://docs.github.com/en/actions/security-guides/encrypted-secrets) - with the name `FLY_API_TOKEN`. - -- Add a `SESSION_SECRET`, `INTERNAL_COMMAND_TOKEN`, and `HONEYPOT_SECRET` to - your fly app secrets, to do this you can run the following commands: - - ```sh - fly secrets set SESSION_SECRET=$(openssl rand -hex 32) INTERNAL_COMMAND_TOKEN=$(openssl rand -hex 32) HONEYPOT_SECRET=$(openssl rand -hex 32) --app [YOUR_APP_NAME] - fly secrets set SESSION_SECRET=$(openssl rand -hex 32) INTERNAL_COMMAND_TOKEN=$(openssl rand -hex 32) HONEYPOT_SECRET=$(openssl rand -hex 32) --app [YOUR_APP_NAME]-staging - ``` - - > **Note**: If you don't have openssl installed, you can also use - > [1Password](https://1password.com/password-generator) to generate a random - > secret, just replace `$(openssl rand -hex 32)` with the generated secret. - -6. Create production database: - - Create a persistent volume for the sqlite database for both your staging and - production environments. Run the following (feel free to change the GB size - based on your needs and the region of your choice - (`https://fly.io/docs/reference/regions/`). If you do change the region, make - sure you change the `primary_region` in fly.toml as well): - - ```sh - fly volumes create data --region sjc --size 1 --app [YOUR_APP_NAME] - fly volumes create data --region sjc --size 1 --app [YOUR_APP_NAME]-staging - ``` - -7. Attach Consul: - -- Consul is a fly-managed service that manages your primary instance for data - replication - ([learn more about configuring consul](https://fly.io/docs/litefs/getting-started/#lease-configuration)). - - ```sh - fly consul attach --app [YOUR_APP_NAME] - fly consul attach --app [YOUR_APP_NAME]-staging - ``` - -8. Commit! - - The Epic Stack comes with a GitHub Action that handles automatically - deploying your app to production and staging environments. - - Now that everything is set up you can commit and push your changes to your - repo. Every commit to your `main` branch will trigger a deployment to your - production environment, and every commit to your `dev` branch will trigger a - deployment to your staging environment. - ---- - -### Optional: Email service setup - -Find instructions for this optional step in [the email docs](./email.md). - -### Optional: Error monitoring setup - -Find instructions for this optional step in -[the error tracking docs](./monitoring.md). - -### Optional: Connecting to your production database - -Find instructions for this optional step in [the database docs](./database.md). - -### Optional: Seeding Production - -Find instructions for this optional step in [the database docs](./database.md). - -## Deploying locally - -If you'd like to deploy locally you definitely can. You need to (temporarily) -move the `Dockerfile` and the `.dockerignore` to the root of the project first. -Then you can run the deploy command: - -``` -mv ./other/Dockerfile Dockerfile -mv ./other/.dockerignore .dockerignore -fly deploy -``` - -Once it's done, move the files back: - -``` -mv Dockerfile ./other/Dockerfile -mv .dockerignore ./other/.dockerignore -``` - -You can keep the `Dockerfile` and `.dockerignore` in the root if you prefer, -just make sure to remove the move step from the `.github/workflows/deploy.yml`. diff --git a/docs/email.md b/docs/email.md deleted file mode 100644 index c42d80f..0000000 --- a/docs/email.md +++ /dev/null @@ -1,22 +0,0 @@ -# Email - -This document describes how to get [Resend](https://resend.com) (the Epic Stack -email provider) setup. - -> **NOTE**: this is an optional step. During development the emails will be -> logged to the terminal and in production if you haven't set the proper -> environment variables yet you will get a warning until you set the environment -> variables. - -Create [an API Key](https://resend.com/api-keys) and set `RESEND_API_KEY` in -both prod and staging: - -```sh -fly secrets set RESEND_API_KEY="re_blAh_blaHBlaHblahBLAhBlAh" --app [YOUR_APP_NAME] -fly secrets set RESEND_API_KEY="re_blAh_blaHBlaHblahBLAhBlAh" --app [YOUR_APP_NAME]-staging -``` - -Setup a [custom sending domain](https://resend.com/domains) and then make sure -to update the `from` email address in `app/utils/email.server.ts` and the -`expect(email.from).toBe` in `tests/e2e/onboarding.test.ts` to the one you want -your emails to come from. diff --git a/docs/examples.md b/docs/examples.md deleted file mode 100644 index 2bbee76..0000000 --- a/docs/examples.md +++ /dev/null @@ -1,119 +0,0 @@ -# Examples - -In keeping with the [guiding principle](guiding-principles.md) to "Minimize -Setup Friction," there are some things that may be pretty common for web -applications to do, but aren't common enough to be included in the main -template. - -This page links to examples of how to implement some things with the Epic Stack. - -- [Framer Motion](https://github.com/kentcdodds/epic-stack-with-framer-motion) - by [@kentcdodds](https://github.com/kentcdodds): Using client hints to avoid - content layout shift with `prefers-reduced-motion` and framer motion - animations. -- [Cross-site Request Forgery Protection (CSRF)](https://github.com/epicweb-dev/epic-stack-example-csrf) - by [@kentcdodds](https://github.com/kentcdodds): An example of the Epic Stack - with CSRF protection on forms. -- [Epic Stack + OpenAI](https://github.com/kentcdodds/epic-ai): by - [@kentcdodds](https://github.com/kentcdodds): An example of the Epic Stack - with OpenAI's GPT API (enhances the notes feature with "generate" buttons). -- [Prisma Client Extensions](https://github.com/L-Steinmacher/epic-stack-with-prisma-client-extensions) - by - [@L-Steinmacher](https://github.com/L-Steinmacher/epic-stack-with-prisma-client-extensions): - An example of the Epic Stack with Prisma Client extensions activated for enum - like behavior in SQLite. -- [Epic Stack + Storybook](https://github.com/moishinetzer/epic-stack-with-storybook): - by [@moishinetzer](https://github.com/moishinetzer): An example of the Epic - Stack with Storybook. It also showcases creating a Remix stub, which is very - helpful for isolating Remix-specific components inside of Storybook. -- [Socket.IO](https://github.com/L-Steinmacher/epic-stack-with-socket.io): by - [@L-Steinmacher](https://github.com/L-Steinmacher): An example of setting up - using websockets in the Epic Stack using the `Socket.IO` library. -- [User Impersonation](https://github.com/alan2207/epic-stack-with-user-impersonation) - by [@alan2207](https://github.com/alan2207): An example Remix application - showcasing how to implement user impersonation in the Epic Stack. -- [Epic Stack + Tailwind CSS Plugin](https://github.com/hakimLyon/epic-stack-with-tailwind-css-plugin) - by [@hakimLyon](https://github.com/hakimLyon): An example of the Epic Stack - with Tailwind CSS Plugin. -- [Epic Stack + GitHub Auth](https://github.com/kentcdodds/epic-github-auth) by - [@kentcdodds](https://github.com/kentcdodds): An example of the Epic Stack - with GitHub Auth. -- [Epic Stack + MongoDB as the Database](https://github.com/hakimLyon/epic-stack-with-prisma-mongodb) - by [@hakimLyon](https://github.com/hakimLyon): An example of the Epic Stack - with Prisma using MongoDB as the database. -- [Epic Stack Custom Themes](https://github.com/kiliman/epic-stack-theme) by - [@kiliman](https://github.com/kiliman): An example showing how to create a - custom theme using the - [`shadcn-custom-theme`](https://github.com/kiliman/shadcn-custom-theme) tool. -- [Epic Stack + OpenID Connect Auth (Google)](https://github.com/kentcdodds/epic-oidc) - by [@kentcdodds](https://github.com/kentcdodds): An example of the Epic Stack - with OpenID Connect Auth (Google) using [web-oidc](https://npm.im/web-oidc) - and [remix-auth](https://npm.im/remix-auth). -- [Epic Stack + Fathom Analytics](https://github.com/xstevenyung/epic-stack-with-fathom-analytics) - by [@xstevenyung](https://github.com/xstevenyung): An example of the Epic - Stack with Fanthom Analytics via CDN -- [Epic Stack + Tenant Users](https://github.com/offseat/epic-stack-tenant) by - [@vinstah](https://github.com/vinstah): An example of the Epic Stack with - tenant users and members starter -- [Epic Stack + i18n](https://github.com/rperon/epic-stack-with-i18n/) by - [@rperon](https://github.com/rperon): An example of the Epic Stack with i18n - using [i18next](https://www.i18next.com/) and - [remix-18next](https://github.com/sergiodxa/remix-i18next) -- [Epic Stack + Argos](https://github.com/jsfez/epic-stack-with-argos) by - [@jsfez](https://github.com/jsfez): An example of the Epic Stack with - [Argos](https://www.argos-ci.com/) for visual testing -- [Epic Stack monorepo with pnpm + turbo](https://github.com/PhilDL/epic-stack-monorepo): - An example of the Epic Stack in a monorepo setup, configs packages, UI - package, and "client-hints" example package. -- [Epic Stack + passkeys/webauthn](https://github.com/rperon/epic-stack-with-passkeys/) - by [@rperon](https://github.com/rperon): An example of the Epic Stack with - passkeys using - [remix-auth-webauthn](https://github.com/alexanderson1993/remix-auth-webauthn) - and [remix-auth](https://npm.im/remix-auth). -- [Epic Stack with jsx-email](https://github.com/djhi/epic-stack-jsx-email): An - example of the Epic Stack that uses [jsx-email](https://jsx.email/) instead of - [react-email](https://react.email/) -- [Epic Stack with Query](https://github.com/gc-victor/epic-stack-with-query) by - [@gc-victor](https://github.com/gc-victor): An example of the Epic Stack using - [Query](https://github.com/gc-victor/query). Using Epic Stack with Query, you - can access the databases directly from your application while enjoying a range - of benefits from Query to manage your databases, including - [Query CLI](https://github.com/gc-victor/query/blob/main/README.md#cli), - [Query API](https://github.com/gc-victor/query/blob/main/README.md#apis), - [Query Funcions](https://github.com/gc-victor/query?tab=readme-ov-file#function) - and [Query Studio](https://github.com/gc-victor/query-studio). -- [`has-js` class](https://github.com/epicweb-dev/epic-stack-example-has-js-class) - by [@kentcdodds](https://github.com/kentcdodds): An example of adding a - `has-js` class on the `` element so you can use that to easily display - different content for users who do not have JavaScript enabled. -- [Confetti](https://github.com/kentcdodds/epic-stack-example-confetti) by - [@kentcdodds](https://github.com/kentcdodds): Display confetti when the user - does something worthy of a celebration. -- [Epic Stack Border animation](https://github.com/hakimLyon/epic-stack-with-css-border-animation) - by [@hakimLyon](https://github.com/hakimLyon): An example of Epic Stack with a - CSS border animation. - -## How to contribute - -[![Kent screencast showing the examples page](https://github.com/epicweb-dev/epic-stack/assets/1500684/7074f1db-c918-42c6-a724-0b082168395f)](https://www.epicweb.dev/tips/contribute-an-epic-stack-example) - -Watch: -[Contribute an Epic Stack Example](https://www.epicweb.dev/tips/contribute-an-epic-stack-example) - -You don't need permission to contribute an example. Feel free to create your own -repository based on the Epic Stack and add whatever you like for your example. -Here are some tips: - -1. Create an `init` commit as soon as you generate the project, before you make - any changes. That way people can look at the commit history of your example - and see what you've added. -2. Update the `README.md` with some information about what your example is about - and call out any interesting things you've done. -3. Add the tags "epic-stack" and "epic-stack-example" so it will appear on - [this page on GitHub](https://github.com/topics/epic-stack-example). -4. It's not normally necessary to deploy your example to production (simply - comment out the deployment part of the GitHub workflow), but you can if you - like. - -Once you've made your repo, simply open a pull request to this page and add your -example to the bottom of the list with a brief description. diff --git a/docs/features.md b/docs/features.md deleted file mode 100644 index c9c046c..0000000 --- a/docs/features.md +++ /dev/null @@ -1,55 +0,0 @@ -# Features - -Here are a few things you get today: - -- [Remix](https://remix.run/) is the Web Framework of choice -- [Fly app deployment](https://fly.io/) with [Docker](https://www.docker.com/) -- Multi-region, distributed, production-ready - [SQLite Database](https://sqlite.org/) with - [LiteFS](https://fly.io/docs/litefs/). -- Healthcheck endpoint for - [Fly backups region fallbacks](https://fly.io/docs/reference/configuration/#services-http_checks) -- [Grafana](https://grafana.com/) Dashboards of the running app -- [GitHub Actions](https://github.com/features/actions) with testing and deploy - on merge for both production and staging environments -- Email/Password Authentication with - [cookie-based sessions](https://remix.run/utils/sessions#md-createcookiesessionstorage) -- Two-Factor Authentication (2fa) with support for authenticator apps. -- Transactional email with [Resend](https://resend.com/) and forgot - password/password reset support. -- Progressively Enhanced and fully type safe forms with - [Conform](https://conform.guide/) -- Database ORM with [Prisma](https://prisma.io/) -- Role-based User Permissions. -- Custom built image hosting -- Caching via [cachified](https://npm.im/cachified): Both in-memory and - SQLite-based (with - [better-sqlite3](https://github.com/WiseLibs/better-sqlite3)) -- Styling with [Tailwind](https://tailwindcss.com/) -- An excellent, customizable component library with - [Radix UI](https://www.radix-ui.com/) -- End-to-end testing with [Playwright](https://playwright.dev/) -- Local third party request mocking with [MSW](https://mswjs.io/) -- Unit testing with [Vitest](https://vitest.dev/) and - [Testing Library](https://testing-library.com/) with pre-configured Test - Database -- Code formatting with [Prettier](https://prettier.io/) -- Linting with [ESLint](https://eslint.org/) -- Static Types with [TypeScript](https://typescriptlang.org/) -- Runtime schema validation with [zod](https://zod.dev/) -- Error monitoring with [Sentry](https://sentry.io/welcome/) -- Light/Dark/System mode (without a flash of incorrect theme) - -Here are some things that will likely find their way into the Epic Stack (or the -docs examples) in the future: - -- Logging -- Ecommerce support with [Stripe](https://stripe.com/) -- Ethical site analytics with [fathom](https://usefathom.com/) -- Internationalization -- Image optimization route and component -- Feature flags -- Documentation on production data seeding process - -Not a fan of bits of the stack? Fork it, change it, and use -`npx create-remix --template your/repo`! Make it your own. diff --git a/docs/fonts.md b/docs/fonts.md deleted file mode 100644 index f86c0df..0000000 --- a/docs/fonts.md +++ /dev/null @@ -1,152 +0,0 @@ -# Fonts - -The Epic Stack uses the default font that comes with Tailwind CSS. This is a -good default font but you may want to use a custom font for your site and it's -easy to do so. - -## Using Custom Fonts - -You can use custom fonts by adding them to the `./public/fonts` directory, -[Google Fonts](https://fonts.google.com/) is a good place to find open source -fonts. You will also need to add the `css` file for the font to the -`./app/styles` directory, if your font doesn't come with one (Google Fonts -don't) you can generate one using a tool like -[Transfonter](https://transfonter.org/). - -You may need to edit the `url` property in the `css` file to point to the -correct location of the font files, that path is relative to the `public` -folder. So it should look something like -`url('/fonts/yourfont/yourfont-200.woff2')`. - -Now you've added your font, there's a few places you need to update to use it. - -1. Add your font to the CSS variables. - - ```css - /* tailwind.css */ - @layer base { - :root { - --font-sans: ; - } - } - ``` - -2. Add your font to the `fontFamily` property. - - ```ts - // tailwind.config.ts - extend: { - ... - fontFamily: { - ... - sans: ['var(--font-sans)', ...defaultTheme.fontFamily.sans], - } - } - - ``` - -3. Import your font stylesheet. - - ```tsx - // root.tsx - import fontStyleSheetUrl from './styles/yourfont.css' - ``` - - Add the font stylesheet to the links array. - - ```tsx - // root.tsx - ... - { rel: 'preload', href: fontStyleSheetUrl, as: 'style' }, - { rel: 'stylesheet', href: fontStyleSheetUrl }, - ``` - -4. Expose and cache your fonts folder. - - ```ts - // server.ts - ... - app.use( - '/fonts', - // Can aggressively cache fonts as they don't change often - express.static('public/fonts', { immutable: true, maxAge: '1y' }), - ) - ``` - -That's it! You can now use your custom font should now be available to use in -your site. - -## Font Metric Overrides - -When using custom fonts, your site elements may stretch or shrink to accommodate -the font. This is because the browser doesn't know the dimensions of the font -you're using until it arrives, which introduces Cumulative Layout Shift and -impact its web vitals. - -In Epic Stack, we fixed this by introducing -[Font Metric Overrides](https://github.com/epicweb-dev/epic-stack/pull/128/files). - -Follow the steps below to add Font Metric Overrides to your custom fonts. - -1. Generate the overrides for your font. - - You can use [fontpie](https://www.npmjs.com/package/fontpie) utility to - generate the overrides. For each of your fonts, write the following in your - terminal: - - ```bash - npx fontpie ./local/font/location.woff2 -w font-weight -s normal/italic -n YourFont - ``` - - #### Example - - ```sh - npx fontpie ./public/fonts/nunito-sans/nunito-sans-v12-latin_latin-ext-200.woff2 -w 200 -s normal -n NunitoSans - ``` - - ```css - @font-face { - font-family: 'NunitoSans Fallback'; - font-style: normal; - font-weight: 200; - src: local('Arial'); - ascent-override: 103.02%; - descent-override: 35.97%; - line-gap-override: 0%; - size-adjust: 98.13%; - } - ``` - - If you've got a lot of font files to override, you can use - [fontpie-from-css](https://github.com/matt-kinton/fontpie-from-css) to - generate the overrides from a CSS file. - - ```sh - npx fontpie-from-css ./public/fonts/yourfont/yourfont.css - ``` - - **_Note:_** _If you've been following the steps above, you might have to copy - your `yourfont.css` file temporarily to the `./public` folder as - `fontpie-from-css` loads fonts relative to the CSS file._ - -2. Add the overrides to your font stylesheet. - - Use fontpie for every custom font used (including variants) or - fontpie-from-css and add the metric overrides to `yourfont.css`. - - _Ensure the original font has the `font-display: swap` property or the - fallback wouldn't work!_ - -3. Add the font fallback to the stylesheet. - - ```css - /* tailwind.css */ - @layer base { - :root { - --font-sans: ; - } - } - ``` - -That's it! You can now use your custom font without worrying about Cumulative -Layout Shift! diff --git a/docs/getting-started.md b/docs/getting-started.md deleted file mode 100644 index bbecf01..0000000 --- a/docs/getting-started.md +++ /dev/null @@ -1,60 +0,0 @@ -# Getting Started with the Epic Stack - -The Epic Stack is a [Remix Stack](https://remix.run/stacks). To start your Epic -Stack, run the following [`npx`](https://docs.npmjs.com/cli/v9/commands/npx) -command: - -```sh -npx create-epic-app@latest -``` - -This will prompt you for a project name (the name of the directory to put your -project). Once you've selected that, the CLI will start the setup process. - -Once the setup is complete, go ahead and `cd` into the new project directory and -run `npm run dev` to get the app started. - -Check the project README.md for instructions on getting the app deployed. You'll -want to get this done early in the process to make sure you're all set up -properly. - -If you'd like to skip some of the setup steps, you can set the following -environment variables when you run the script: - -- `SKIP_SETUP` - skips running `npm run setup` -- `SKIP_FORMAT` - skips running `npm run format` -- `SKIP_DEPLOYMENT` - skips deployment setup - -So, if you enabled all of these it would be: - -```sh -SKIP_SETUP=true SKIP_FORMAT=true SKIP_DEPLOYMENT=true npx create-epic-app@latest -``` - -Or, on windows: - -``` -set SKIP_SETUP=true && set SKIP_FORMAT=true && set SKIP_DEPLOYMENT=true && npx create-epic-app@latest -``` - -## Development - -- Initial setup: - - ```sh - npm run setup - ``` - -- Start dev server: - - ```sh - npm run dev - ``` - -This starts your app in development mode, rebuilding assets on file changes. - -The database seed script creates a new user with some data you can use to get -started: - -- Username: `kody` -- Password: `kodylovesyou` diff --git a/docs/guiding-principles.md b/docs/guiding-principles.md deleted file mode 100644 index 4e22222..0000000 --- a/docs/guiding-principles.md +++ /dev/null @@ -1,30 +0,0 @@ -# Epic Stack Guiding Principles - -Decisions about the Epic Stack should be guided by the following guiding -principles: - -- **Limit Services:** If we can reasonably build, deploy, maintain it ourselves, - do it. Additionally, if we can reasonably run it within our app instance, do - it. This saves on cost and reduces complexity. -- **Include Only Most Common Use Cases:** As a project generator, it is expected - that some code will necessarily be deleted, but implementing support for every - possible type of feature is literally impossible. _The starter app is not - docs_, so to demonstrate a feature or give an example, put that in the docs - instead of in the starter app. -- **Minimize Setup Friction:** Try to keep the amount of time it takes to get an - app to production as small as possible. If a service is necessary, see if we - can defer signup for that service until its services are actually required. - Additionally, while the target audience for this stack is apps that need scale - you have to pay for, we try to fit within the free tier of any services used - during the exploration phase. -- **Optimize for Adaptability:** While we feel great about our opinions, - ever-changing product requirements sometimes necessitate swapping trade-offs. - So while we try to keep things simple, we want to ensure teams using the Epic - Stack are able to adapt by switching between third party services to - custom-built services and vice-versa. -- **Only one way:** Avoid providing more than one way to do the same thing. This - applies to both the pre-configured code and the documentation. -- **Offline Development:** We want to enable offline development as much as - possible. Naturally we need to use third party services for some things (like - email), but for those we'll strive to provide a way to mock them out for local - development. diff --git a/docs/icons.md b/docs/icons.md deleted file mode 100644 index c0f8850..0000000 --- a/docs/icons.md +++ /dev/null @@ -1,41 +0,0 @@ -# Icons - -The Epic Stack uses SVG sprites for -[optimal icon performance](https://benadam.me/thoughts/react-svg-sprites/). -You'll find raw SVGs in the `./other/svg-icons` directory. These are then -compiled into a sprite using the `npm run build:icons` script which generates -the `icon.svg` file and an `icons.json` manifest file that allows Typescript to -pick up the names of the icons. - -You can use [Sly](https://github.com/jacobparis-insiders/sly/tree/main/cli) to -add new icons from the command line. - -To add the `trash`, `pencil-1`, and `avatar` icons, run: - -```sh -npx sly add @radix-ui/icons trash pencil-1 avatar -``` - -If you don't specify the icons, Sly will show an interactive list of all the -icons available in the `@radix-ui/icons` collection and let you select the ones -you want to add. - -Sly has been configured in the Epic Stack to automatically add the icons to the -`./other/svg-icons` directory and to run the `npm run build:icons` script -automatically, so there are no extra steps to take. You can see the -configuration in the `./other/sly/sly.json` file. - -The SVGs used by default in the Epic Stack come from -[icons.radix-ui.com](https://icons.radix-ui.com/). You can download additional -SVG icons from there, or provide your own. Once you've added new files in the -directory, run `npm run build:icons` and you can then use the `Icon` component -to render it. The `icon` prop is the name of the file without the `.svg` -extension. We recommend using `kebab-case` filenames rather than `PascalCase` to -avoid casing issues with different operating systems. - -Note that the `./other/build-icons.ts` file automatically removes `width` and -`height` props from your SVGs to ensure they scale properly. - -By default, all the icons will have a height and width of `1em` so they should -match the font-size of the text they're next to. You can also customize the size -using the `size` prop. diff --git a/docs/managing-updates.md b/docs/managing-updates.md deleted file mode 100644 index 3a35fe4..0000000 --- a/docs/managing-updates.md +++ /dev/null @@ -1,175 +0,0 @@ -# Managing updates - -## Updating Node.js - -The Epic Stack runs a long-running Node.js server. It defaults to the current -active LTS version of node -([read the Node.js version decision document](./decisions/021-node-version.md)). - -If you wish to change the Node.js version, you can do so by updating the -`engines.node` property in the `package.json` file. - -```json -{ - "engines": { - "node": "20.3.1" - } -} -``` - -Make certain you do not use a version range here because this is used in the -`./other/build-server.ts` to compile the express server code. - -You will also want to update the `Dockerfile` to use the same version of Node.js -as the `package.json` file. - -```diff -- FROM node:18-bookworm-slim as base -+ FROM node:20.3.1-bookworm-slim as base -``` - -You'll find the -[Node.js versions available on Docker Hub](https://hub.docker.com/_/node). - -## Within the Epic Stack - -When you create a new project with the Epic Stack, a bunch of code is generated -for you. This code is completely yours and there is no way to update it other -than making manual changes. This is both a good thing and a bad thing. It's good -in the sense that you can tweak it to fit your specific use cases. But it's a -challenge because as the Epic Stack gets improvements there's no way to get -those automatically. You have to keep track of the improvements in the Epic -Stack and make those updates yourself. - -You shouldn't feel compelled to keep up-to-date with the latest of the Epic -Stack template. If what you're using is working fine for you then just keep -going with it. Only adopt changes as you feel the need to do so. Feel free to -peruse -[the Epic Stack's commit history](https://github.com/epicweb-dev/epic-stack/commits/main) -anytime you'd like to see what updates could be made to your project. - -## How to update NPM dependencies - -Another part of the Epic Stack is the dependencies of the project. These you -will also have to keep up-to-date yourself, but there is a bit of an automated -process to help you. - -It’s important to update your packages to get new features, bug fixes, and -security patches. -[NPM Check Updates](https://www.npmjs.com/package/npm-check-updates) is a CLI -that will help you safely make those updates. You can watch -[this youtube video](https://www.youtube.com/watch?v=0XQXGx3lLaU) for a -demonstration of how to do this. - -### See a list of packages that can be updated - -NPM packages follow [semantic versioning](https://semver.org). This command will -show you which packages can be updated and which major, minor, or patch versions -are available. - -```sh -npx npm-check-updates -``` - -Notice the colors: - -- Green = (non-major version zero) patch updates -- Cyan = minor updates -- Red = major or [version zero (0.y.z)](https://semver.org/#spec-item-4) updates - -### Update green patch versions first, all at once - -Since green patch version updates are meant for backward-compatible bug fixes, -it's ok to update them all at once. - -```sh -npx npm-check-updates -u --target patch -... -npm i -``` - -> Note: `npx npm-check-updates -u -t patch` updates all patch versions, -> including major version zero patch versions, which can break your code. If all -> your patch updates are green, feel free to use this command instead to update -> them all at once. - -Assuming package maintainers follow semantic versioning, updating patch versions -shouldn't break anything, but it's good practice to re-run your tests before -committing these changes. - -```sh -npm run test -- run -npm run test:e2e:run -``` - -If all tests pass, commit your changes. - -```sh -git add . -git commit -m "Updated patch versions" -``` - -### Update cyan minor versions second, one by one - -Minor version updates introduce new features in a backward-compatible way. This -is exciting and it's good practice to take some time to explore the new -functionality and apply relevant updates to your code base or plan to apply them -later. It's recommended you do this package by package instead of all at once. - -To check for the new package's features, check its release notes on GitHub. - -> If you haven't updated a fairly active package in a while, reading all its -> release notes can take some time. Take into consideration how important a -> package is for your project when choosing which to update first. - -```sh -npx npm-check-updates -u --filter -npm i -``` - -Again, assuming package maintainers follow semantic versioning updating patch -versions shouldn't break anything, but it's good practice to re-run your tests -to make sure. - -```sh -npm run test -- run -npm run test:e2e:run -``` - -If all tests pass, commit your changes. - -```sh -git add . -git commit -m "Updated minor versions" -``` - -### Update red versions third, one by one - -Red updates can happen on patch or minor versions (for zero major version -(0.y.z) packages) or major versions. Either way, they could be breaking changes. -It's recommended you read its release notes to see what changed and plan -accordingly. - -> Again, you might want to take into consideration how important a package is -> for your project when choosing which to update first. - -```sh -npx npm-check-updates -u -f -npm i -``` - -Make sure you've made all relevant changes and that the tests pass. - -```sh -npm run test -- run -npm run test:e2e:run -``` - -If all tests pass, commit your changes. - -```sh -git add . -git commit -m "Updated major version" -``` - -Then continue for each package. diff --git a/docs/memory.md b/docs/memory.md deleted file mode 100644 index 76a914d..0000000 --- a/docs/memory.md +++ /dev/null @@ -1,17 +0,0 @@ -# Memory - -Epic Stack apps start with a single instance with 256MB of memory. This is a -pretty small amount of memory, but it's enough to get started with. To help -avoid memory pressure even at that scale, we allocate a 512MB swap file. Learn -more about this decision in -[the memory swap decision document](decisions/010-memory-swap.md). - -To modify or increase the swap file, check `other/setup-swap.js`. This file is -executed before running our app within the `litefs.yml` config. - -> **NOTE**: PRs welcome to document how to determine the effectiveness of the -> swap file for your app. - -To increase the memory allocated to your vm, use the -[`fly scale`](https://fly.io/docs/flyctl/scale-memory/) command. You can -[learn more about memory sizing in the Fly docs](https://fly.io/docs/machines/guides-examples/machine-sizing). diff --git a/docs/monitoring.md b/docs/monitoring.md deleted file mode 100644 index 79807a2..0000000 --- a/docs/monitoring.md +++ /dev/null @@ -1,41 +0,0 @@ -# Monitoring - -This document describes how to get [Sentry](https://sentry.io/) (the Epic -application monitoring provider) set up for error, performance, and replay -monitoring. - -> **NOTE**: this is an optional step and only needed if you want monitoring in -> production. - -## SaaS vs Self-Hosted - -Sentry offers both a [SaaS solution](https://sentry.io/) and -[self-hosted solution](https://develop.sentry.dev/self-hosted/). This guide -assumes you are using SaaS but the guide still works with self-hosted with a few -modifications. - -## Signup - -You can sign up for Sentry and create a Remix project from visiting -[this url](https://sentry.io/signup/?project_platform=javascript-remix) and -filling out the signup form. - -## Onboarding - -Once you see the onboarding page which has the DSN, copy that somewhere (this -becomes `SENTRY_DSN`). Then click -[this](https://sentry.io/orgredirect/settings/:orgslug/developer-settings/new-internal/) -to create an internal integration. Give it a name and add the scope for -`Releases:Admin`. Press Save, find the auth token at the bottom of the page -under "Tokens", and copy that to secure location (this becomes -`SENTRY_AUTH_TOKEN`). Then visit the organization settings page and copy that -organization slug (`SENTRY_ORG_SLUG`). - -Now, set the secrets in Fly.io: - -```sh -fly secrets set SENTRY_DSN= SENTRY_AUTH_TOKEN= SENTRY_ORG= SENTRY_PROJECT=javascript-remix -``` - -Note that `javascript-remix` is the name of the default Remix project in Sentry -and if you use a different project name you'll need to update that value here. diff --git a/docs/permissions.md b/docs/permissions.md deleted file mode 100644 index a60f78d..0000000 --- a/docs/permissions.md +++ /dev/null @@ -1,42 +0,0 @@ -# Permissions - -The Epic Stack's Permissions model takes after -[Role-Based Access Control (RBAC)](https://auth0.com/intro-to-iam/what-is-role-based-access-control-rbac). -Each user has a set of roles, and each role has a set of permissions. A user's -permissions are the union of the permissions of all their roles (with the more -permissive permission taking precedence). - -The default development seed creates fine-grained permissions that include -`create`, `read`, `update`, and `delete` permissions for `user` and `note` with -the access of `own` and `any`. The default seed also creates `user` and `admin` -roles with the sensible permissions for those roles. - -You can combine these permissions in different ways to support different roles -for different personas of users of your application. - -The Epic Stack comes with built-in utilities for working with these permissions. -Here are some examples to give you an idea: - -```ts -// server-side only utilities -const userCanDeleteAnyUser = await requireUserWithPermission( - request, - 'delete:user:any', -) -const userIsAdmin = await requireUserWithRole(request, 'admin') -``` - -```ts -// UI utilities -const user = useUser() -const userCanCreateTheirOwnNotes = userHasPermission(user, 'create:note:own') -const userIsUser = userHasRole(user, 'user') -``` - -There is currently no UI for managing permissions, but you can use prisma studio -for establishing these. - -## Seeding the production database - -Check [the deployment docs](./deployment.md) for instructions on how to seed the -production database with the roles you want. diff --git a/docs/redirects.md b/docs/redirects.md deleted file mode 100644 index 3667332..0000000 --- a/docs/redirects.md +++ /dev/null @@ -1,88 +0,0 @@ -# Redirects - -We handle redirects in Express. Incoming requests are handled by Express before -they pass through to Remix, so by redirecting earlier you improve performance. - -## HTTP to HTTPS - -We force all traffic to HTTPS automatically. That way, no part of your -application is open to request interception. This does not affect localhost, as -we use Fly's request headers for determining when to redirect. - -```ts -app.use((req, res, next) => { - const proto = req.get('X-Forwarded-Proto') - const host = getHost(req) - if (proto === 'http') { - res.set('X-Forwarded-Proto', 'https') - res.redirect(`https://${host}${req.originalUrl}`) - return - } - next() -}) -``` - -## Remove trailing slashes - -We also remove trailing slashes automatically. A url like -`https://example.com/foo/` is automatically redirected to -`https://example.com/foo`. - -This is important for SEO reasons, as website crawlers (like Google) treat these -as separate URLs and will consider them to be distinct pages with duplicate -content. - -```ts -app.use((req, res, next) => { - if (req.path.endsWith('/') && req.path.length > 1) { - const query = req.url.slice(req.path.length) - const safepath = req.path.slice(0, -1).replace(/\/+/g, '/') - res.redirect(301, safepath + query) - } else { - next() - } -}) -``` - -## www subdomains - -You can redirect root domain traffic to your www subdomain or vice versa. - -[DNS level redirects do not work with Fly](https://community.fly.io/t/how-to-redirect-from-non-www-to-www/5795/2). -The recommended way to implement this redirect is in your application code. - -First, you will need to register SSL certificates for both the www and the root -domain. You can -[register an SSL certificate for your domains in the terminal](https://fly.io/docs/getting-started/working-with-fly-apps/#fly-io-and-custom-domains) -or in your [Fly dashboard](https://fly.io/dashboard/) by navigating to your app -and selecting "Certificates" on the sidebar. - -After registering the two SSL certificates, Fly will allow traffic from either -source to hit your application, and then you can redirect to your preferred -domain in code. - -**To redirect non-www traffic to www** - -```ts -app.use((req, res, next) => { - const host = getHost(req) - if (!host.startsWith('www.')) { - return res.redirect(301, `https://www.${host}${req.url}`) - } else { - next() - } -}) -``` - -**To redirect www traffic to non-www:** - -```ts -app.use((req, res, next) => { - const host = getHost(req) - if (host.startsWith('www.')) { - return res.redirect(301, `https://${host.slice(4)}${req.url}`) - } else { - next() - } -}) -``` diff --git a/docs/routing.md b/docs/routing.md deleted file mode 100644 index b8a043d..0000000 --- a/docs/routing.md +++ /dev/null @@ -1,145 +0,0 @@ -# Routing - -The Epic Stack uses file-based routing with Remix. However, it's not using the -built-in routing convention of Remix and instead is using -[remix-flat-routes](https://github.com/kiliman/remix-flat-routes) which is a -special implementation of the Remix convention that adds a few features. You'll -find it configured for the application in the `remix.config.js` file at the root -of the app. Specifically "hybrid routes." - -We'll defer to the `remix-flat-routes` documentation for specifics, but an -important thing for you to know as you get used to this convention is you can -always run `npx remix routes` from the root of the app and it will output the -routes of your application in a JSX-like output that will reveal the routes that -will be generated based on your current file structure. Here's an example of the -Epic Stack routes at the time of this writing: - -``` -app/routes -β”œβ”€β”€ _auth+ -β”‚Β Β  β”œβ”€β”€ forgot-password.tsx -β”‚Β Β  β”œβ”€β”€ login.tsx -β”‚Β Β  β”œβ”€β”€ logout.tsx -β”‚Β Β  β”œβ”€β”€ onboarding.tsx -β”‚Β Β  β”œβ”€β”€ reset-password.tsx -β”‚Β Β  └── signup.tsx -β”œβ”€β”€ _marketing+ -β”‚Β Β  β”œβ”€β”€ about.tsx -β”‚Β Β  β”œβ”€β”€ index.tsx -β”‚Β Β  β”œβ”€β”€ logos -β”‚Β Β  β”‚Β Β  β”œβ”€β”€ logos.ts -β”‚Β Β  β”‚Β Β  └── ... -β”‚Β Β  β”œβ”€β”€ privacy.tsx -β”‚Β Β  β”œβ”€β”€ support.tsx -β”‚Β Β  └── tos.tsx -β”œβ”€β”€ admin+ -β”‚Β Β  β”œβ”€β”€ cache.tsx -β”‚Β Β  β”œβ”€β”€ cache_.lru.$cacheKey.ts -β”‚Β Β  β”œβ”€β”€ cache_.sqlite.$cacheKey.ts -β”‚Β Β  └── cache_.sqlite.tsx -β”œβ”€β”€ me.tsx -β”œβ”€β”€ resources+ -β”‚Β Β  β”œβ”€β”€ delete-image.test.tsx -β”‚Β Β  β”œβ”€β”€ delete-image.tsx -β”‚Β Β  β”œβ”€β”€ delete-note.tsx -β”‚Β Β  β”œβ”€β”€ file.$fileId.tsx -β”‚Β Β  β”œβ”€β”€ healthcheck.tsx -β”‚Β Β  β”œβ”€β”€ image-upload.tsx -β”‚Β Β  β”œβ”€β”€ login.tsx -β”‚Β Β  β”œβ”€β”€ note-editor.tsx -β”‚Β Β  └── theme.tsx -β”œβ”€β”€ settings+ -β”‚Β Β  β”œβ”€β”€ profile.photo.tsx -β”‚Β Β  └── profile.tsx -└── users+ - β”œβ”€β”€ $username.tsx - └── $username_+ - β”œβ”€β”€ notes.$noteId.tsx - β”œβ”€β”€ notes.$noteId_.edit.tsx - β”œβ”€β”€ notes.index.tsx - β”œβ”€β”€ notes.new.tsx - └── notes.tsx - -9 directories, 54 files -``` - -```tsx - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -``` - -Basically, remix-flat-routes hybrid routing allows us to get the best of both -worlds: - -- Colocation of routes to the code they use -- Organized folder structure to keep routes together as needed - -If you're familiar with the Remix routing convention, just think of it this way, -remix-flat-routes converts `+/` to `.`. diff --git a/docs/secrets.md b/docs/secrets.md deleted file mode 100644 index 9aca013..0000000 --- a/docs/secrets.md +++ /dev/null @@ -1,37 +0,0 @@ -# Secrets - -Managing secrets in the Epic Stack is done using environment variables and the -`fly secrets` command. - -> **Warning**: It is very important that you do NOT hard code any secrets in the -> source code. Even if your app source is not public, there are a lot of reasons -> this is dangerous and in the epic stack we default to creating source maps -> which will reveal your hard coded secrets to the public. Read more about this -> in [the source map decision document](./decisions/016-source-maps.md). - -## Local development - -When you need to create a new secret, it's best to add a line to your -`.env.example` file so folks know that secret is necessary. The value you put in -here should be not real because this file is committed to the repository. - -To keep everything in line with the [guiding principle](./guiding-principles.md) -of "Offline Development," you should also strive make it so whatever service -you're interacting with can be mocked out using MSW in the `test/mocks` -directory. - -You can also put the real value of the secret in `.env` which is `.gitignore`d -so you can interact with the real service if you need to during development. - -## Production secrets - -To publish a secret to your production and staging applications, you can use the -`fly secrets set` command. For example, if you were integrating with the `tito` -API, to set the `TITO_API_SECRET` secret, you would run the following command: - -```sh -fly secrets set TITO_API_SECRET=some_secret_value -fly secrets set TITO_API_SECRET=some_secret_value --app [YOUR_STAGING_APP_NAME] -``` - -This will redeploy your app with that environment variable set. diff --git a/docs/security.md b/docs/security.md deleted file mode 100644 index 15a4759..0000000 --- a/docs/security.md +++ /dev/null @@ -1,85 +0,0 @@ -# Security - -The Epic Stack has several security measures in place to protect your users and -yourself. This (incomplete) document, explains some of the security measures -that are in place and how to use them. - -## Content Security Policy - -The Epic Stack uses a strict -[Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP). -This means that only resources from trusted sources are allowed to be loaded. -However, by default, the CSP is set to `report-only` which means that the -browser will report violations of the CSP without actually blocking the -resource. - -This is to prevent new users of the Epic Stack from being blocked or surprised -by the CSP by default. However, it is recommended to enable the CSP in -`server/index.ts` by removing the `reportOnly: true` option. - -## Fly's Internal Network - -The Epic Stack uses [Fly](https://fly.io) for hosting. Fly has an internal -network that allows you to connect services to each other without exposing them -to the public internet. Only services within your organization have access to -this network, and only accounts in your organization have access as well. - -When running multiple instances of the Epic Stack, your instances communicate -with each other over this internal network. Most of this happens behind the -scenes with the consul service that Fly manages for us. - -We also have an endpoint that allows instances to connect to each other to -update the cache in the primary region. This uses internal URLs for that -communication (via [`litefs-js`](https://github.com/fly-apps/litefs-js)), but as -an added layer of security it uses a shared secret to validate the requests. - -> This could be changed if there's a way to determine if a request is coming -> from the internal network. But I haven't found a way to do that yet. PRs -> welcome! - -Outside of this, the Epic Stack does not access other first-party services or -databases. - -## Secrets - -The currently recommended policy for managing secrets is to place them in a -`.env` file in the root of the application (which is `.gitignore`d). There is a -`.env.example` which can be used as a template for this file (and if you do not -need to actually connect to real services, this can be used as -`cp .env.example .env`). - -These secrets need to also be set on Fly using the `fly secrets` command. - -There are significant limitations to this approach and will probably be improved -in the future. - -## [Cross-Site Scripting (XSS)](https://developer.mozilla.org/en-US/docs/Glossary/Cross-site_scripting) - -React has built-in support for XSS protection. It does this by escaping all -values by default. This means that if you want to render HTML, you need to use -the `dangerouslySetInnerHTML` prop. This is a good thing, but it does mean that -you need to be careful when rendering HTML. Never pass anything that is -user-generated to this prop. - -## [Cross-Site Request Forgery (CSRF)](https://forms.epicweb.dev/07) - -The Epic Stack has built-in support to prevent CSRF attacks. We use the -[`remix-utils`](https://github.com/sergiodxa/remix-utils) -[CSRF-related utilities](https://github.com/sergiodxa/remix-utils#csrf) to do -this. - -## [Honeypot](https://forms.epicweb.dev/06) - -The Epic Stack has built-in support for honeypot fields. We use the -[`remix-utils`](https://github.com/sergiodxa/remix-utils) -[honeypot-related utilities](https://github.com/sergiodxa/remix-utils#form-honeypot) -to do this. - -## Rate Limiting - -The Epic Stack uses a rate limiter to prevent abuse of the API. This is -configured in the `server/index.ts` file and can be changed as needed. By -default it uses [`express-rate-limit`](https://npm.im/express-rate-limit) with -the in-memory store. There are trade-offs with this simpler approach, but it -should be relatively simple to externalize the store into Redis as that's a -built-in feature to express-rate-limit. diff --git a/docs/seo.md b/docs/seo.md deleted file mode 100644 index 3da3b2f..0000000 --- a/docs/seo.md +++ /dev/null @@ -1,41 +0,0 @@ -# SEO - -Remix has built-in support for setting up `meta` tags on a per-route basis which -you can read about -[in the Remix Metadata docs](https://remix.run/docs/en/main/route/meta). - -The Epic Stack also has built-in support for `/robots.txt` and `/sitemap.xml` -via [resource routes](https://remix.run/docs/en/main/guides/resource-routes) -using [`@nasa-gcn/remix-seo`](https://github.com/nasa-gcn/remix-seo). By -default, all routes are included in the `sitemap.xml` file, but you can -configure which routes are included using the `handle` export in the route. Only -public-facing pages should be included in the `sitemap.xml` file. - -Here are two quick examples of how to customize the sitemap on a per-route basis -from the `@nasa-gcn/remix-seo` docs: - -```tsx -// routes/blog/$blogslug.tsx - -export const handle: SEOHandle = { - getSitemapEntries: async request => { - const blogs = await db.blog.findMany() - return blogs.map(blog => { - return { route: `/blog/${blog.slug}`, priority: 0.7 } - }) - }, -} -``` - -```tsx -// in your routes/url-that-doesnt-need-sitemap -import { SEOHandle } from '@nasa-gcn/remix-seo' - -export let loader = ({ request }: LoaderFunctionArgs) => { - /**/ -} - -export const handle: SEOHandle = { - getSitemapEntries: () => null, -} -``` diff --git a/docs/server-timing.md b/docs/server-timing.md deleted file mode 100644 index 248f769..0000000 --- a/docs/server-timing.md +++ /dev/null @@ -1,86 +0,0 @@ -# Server Timing - -![Network tab of Chrome DevTools showing the Timing tab of a specific network call and an arrow pointing to the Server Timing section with the words "This is what server timings do"](https://github.com/epicweb-dev/epic-stack/assets/1500684/e5a28253-8204-43b1-8222-3f287d024ca5) - -The Epic Stack comes with a built-in server timing utility that allows you to -measure the performance of your application. You can find it in the -`app/utils/timing.server.ts` file. The idea is you can wrap a function in a -`time` call and then use the timings object to generate a `Server-Timing` header -which you can then use to have fine grained timing metrics for requests made in -your app. - -You can -[learn more about the Server Timing header on MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Server-Timing). -The metrics passed in this header will be visually displayed in -[the DevTools "Timing" tab](https://developer.chrome.com/docs/devtools/network/reference/#timing). - -## Usage - -Timings requires four parts: - -1. Setup Timings -2. Time functions -3. Create headers -4. Send headers - -Here are all those parts in action in the `/user/:username/notes` route at the -time of this writing: - -```tsx -import { - combineServerTimings, - makeTimings, - time, -} from '#app/utils/timing.server.ts' - -export async function loader({ params }: LoaderFunctionArgs) { - const timings = makeTimings('notes loader') // <-- 1. Setup Timings - // 2. Time functions - const owner = await time( - () => - prisma.user.findUnique({ - where: { - username: params.username, - }, - select: { - id: true, - username: true, - name: true, - imageId: true, - }, - }), - { timings, type: 'find user' }, - ) - if (!owner) { - throw new Response('Not found', { status: 404 }) - } - // 2. Time functions - const notes = await time( - () => - prisma.note.findMany({ - where: { - ownerId: owner.id, - }, - select: { - id: true, - title: true, - }, - }), - { timings, type: 'find notes' }, - ) - return json( - { owner, notes }, - { headers: { 'Server-Timing': timings.toString() } }, // <-- 3. Create headers - ) -} - -export const headers: HeadersFunction = ({ loaderHeaders, parentHeaders }) => { - return { - 'Server-Timing': combineServerTimings(parentHeaders, loaderHeaders), // <-- 4. Send headers - } -} -``` - -You can -[learn more about `headers` in the Remix docs](https://remix.run/docs/en/main/route/headers) -(note, the Epic Stack has the v2 behavior enabled). diff --git a/docs/testing.md b/docs/testing.md deleted file mode 100644 index 8a0b630..0000000 --- a/docs/testing.md +++ /dev/null @@ -1,48 +0,0 @@ -# Testing - -## Playwright - -We use Playwright for our End-to-End tests in this project. You'll find those in -the `tests` directory. As you make changes, add to an existing file or create a -new file in the `tests` directory to test your changes. - -To run these tests in development, run `npm run test:e2e:dev` which will start -the dev server for the app and run Playwright on it. - -We have a fixture for testing authenticated features without having to go -through the login flow: - -```ts -test('my test', async ({ page, login }) => { - const user = await login() - // you are now logged in -}) -``` - -We also auto-delete the user at the end of your test. That way, we can keep your -local db clean and keep your tests isolated from one another. - -## Vitest - -For lower level tests of utilities and individual components, we use `vitest`. -We have DOM-specific assertion helpers via -[`@testing-library/jest-dom`](https://testing-library.com/jest-dom). - -## Type Checking - -This project uses TypeScript. It's recommended to get TypeScript set up for your -editor to get a really great in-editor experience with type checking and -auto-complete. To run type checking across the whole project, run -`npm run typecheck`. - -## Linting - -This project uses ESLint for linting. That is configured in `.eslintrc.js`. - -## Formatting - -We use [Prettier](https://prettier.io/) for auto-formatting in this project. -It's recommended to install an editor plugin (like the -[VSCode Prettier plugin](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode)) -to get auto-formatting on save. There's also a `npm run format` script you can -run to format all files in the project. diff --git a/docs/timezone.md b/docs/timezone.md deleted file mode 100644 index 4234892..0000000 --- a/docs/timezone.md +++ /dev/null @@ -1,37 +0,0 @@ -# Timezones - -Server rendering timezones has always been a pain. This is because the server -doesn't know the user's timezone. It only knows the timezone of the server. So -lots of people will take the easy way out and do one of the following -workarounds: - -- Just render in UTC: Not great because it's not the user's timezone -- Render in the server's timezone: Not great because it's not the user's - timezone -- Render in the server's timezone, and hydrate in the client's timezone: Not - great because it causes a flash of incorrect content (and a hydration error - unless you add `suppressHydrationWarning={true}` to the element) -- Don't render the time on the server at all: Not great because it's a flash of - incomplete content (and no, fading it in does not count). -- Only render the time from user interaction: Sometimes this is fine, but often - you're just compromising on UX and you know it. - -Thanks to the Epic Stack's built-in support for -[client hints](./client-hints.md), we can do better! We have a client hint set -up for the user's timezone. This means you can render the time on the server in -the user's timezone, and hydrate it in the user's timezone, without any flash of -incorrect content or hydration errors. - -You can use this in a few ways. In server-side only code, -`getHints(request).timeZone` will be what you're looking for. In UI code, you -can use `useHints().timeZone` to get the user's timezone. - -For the server-side code, we have a `getDateTimeFormat` utility uses to give you -a -[`DateTimeFormat`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat) -object that is in the user's timezone (it also uses the standard -`accept-language` header to determine the user's preferred locale). - -If you'd prefer to use a library for formatting dates and times, feel free to -simply access the timezone from the hints and use it with your library of -choice. diff --git a/docs/toasts.md b/docs/toasts.md deleted file mode 100644 index ccba4da..0000000 --- a/docs/toasts.md +++ /dev/null @@ -1,62 +0,0 @@ -# Toasts - -Toast messages are great ways to temporarily call someone's attention to -something. They are often used to notify users of a successful or failed action. - -![toasts](https://github.com/epicweb-dev/epic-stack/assets/1500684/715d754a-9e9f-4b61-814f-881121f2fa48) - -There are utilities in the Epic Stack for toast notifications. - -This is managed by a special session using a concept called "flash data" which -is a temporary session value that is only available for the next request. This -is a great way to pass data to the next request without having to worry about -the data persisting in the session. And you don't have to worry about managing -state either. It all just lives in the cookie. - -The primary utility you'll use for redirecting with toast notifications is -`redirectWithToast` from `app/utils/toast.server.ts`. Here's a simple example of -using this: - -```tsx -return redirectWithToast(`/users/${note.owner.username}/notes/${note.id}`, { - description: id ? 'Note updated' : 'Note created', -}) -``` - -This accepts an additional argument for other `ResponseInit` options so you can -set other headers, etc. - -If you don't wish to redirect, you could use the underlying `createToastHeaders` -directly: - -```tsx -return json( - { success: true }, - { - headers: await createToastHeaders({ - description: 'Note updated', - type: 'success', - }), - }, -) -``` - -And if you need to set multiple headers, you can use the `combineHeaders` -utility from `app/utils/misc.tsx`: - -```tsx -return json( - { success: true }, - { - headers: combineHeaders( - await createToastHeaders({ - toast: { - description: 'Note updated', - type: 'success', - }, - }), - { 'x-foo': 'bar' }, - ), - }, -) -``` diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md deleted file mode 100644 index e316165..0000000 --- a/docs/troubleshooting.md +++ /dev/null @@ -1,47 +0,0 @@ -# Troubleshooting - -This is the page where we document common errors and how to fix them: - -## Content Security Policy violations - -If you've received an error like this: - -> Refused to load the image 'https://example.com/thing.png' because it violates -> the following Content Security Policy directive: "img-src 'self'". - -This means you're trying to add a link to a resource that is not allowed. Learn -more about the decision to add this content security policy (CSP) in -[the decision document](./decisions/008-content-security-policy.md). NOTE: This -is disabled by default as of -[the report-only CSP decision](./decisions/022-report-only-csp.md). It is, -however, recommended to be enabled for security reasons. - -To fix this, adjust the CSP to allow the resource you're trying to add. This can -be done in the `server/index.ts` file. - -```diff - contentSecurityPolicy: { - directives: { - 'connect-src': [ - MODE === 'development' ? 'ws:' : null, - process.env.SENTRY_DSN ? '*.ingest.sentry.io' : null, - "'self'", - ].filter(Boolean), - 'font-src': ["'self'"], - 'frame-src': ["'self'"], -- 'img-src': ["'self'", 'data:'], -+ 'img-src': ["'self'", 'data:', 'https://*.example.com'] -``` - -## Missing Icons - -Epic Stack uses SVG sprite icons for performance reasons. If you've received an -error like this during local development: - -> X [ERROR] Could not resolve "#app/components/ui/icon.tsx" - -You need to be manually regenerate the icon with `npm run build:icons`. - -See -[the icons decision document](https://github.com/epicweb-dev/epic-stack/blob/main/docs/decisions/020-icons.md) -for more information about icons.