Skip to content

Latest commit

 

History

History
351 lines (242 loc) · 12.3 KB

File metadata and controls

351 lines (242 loc) · 12.3 KB

Stacks

The following stacks are still in decision phase:

  • HTMX
  • Alpine AJAX
  • Apollo GraphQL Client
  • PostgreSQL
  • Tailwind CSS
  • GraphQL
  • Supabase
  • Resend - for email verification sending (SMTP)
  • Pion (WebRTC)

Hosting:

  • Vercel
  • Netlify
  • GitHub Pages
  • Cloudflare Pages
  • Digital Ocean

Target Platforms

  • Web Browser (Chome, Firefox, Safari)
  • Mobile (Android, iOS)

Features

  • When a channel is deleted, the deleted_at column in channels table is set but the record still exist ( soft-delete). The channel still visible to user with ADMIN role type.
  • If a user is removed from a channel, the deleted_at column in channel_members is only set. The record will remain in the table. If the user is added back to the channel, the deleted_at column will be set to NULL again.
  • The ADMIN can see the message edits history. While the USER can still know if the message is edited but not the edit history.
  • Messages are soft-deleted but the deletedAt time is set in the Message. The ADMIN can still see the deleted messages. While the USER knows that there's a message, but the content is empty.
  • Deleting and editing a message are only allowed for the same user.
  • Replying to a deleted message is allowed. But the content of the deleted message is still not visible (if USER) but visible to ADMIN.
  • For deleted messages, ADMIN can see the content of deleted messages while the USER knows there's a message but the content is an empty string. Neither ADMIN nor USER can edit a deleted message.
    • On the frontend, the content of the deleted message can be toggled to visible or not through a button (setting).

Local Development

Secrets and Configurations Management

It uses 3-layer approach.

  • The flag has the highest priority (e.g. --server-port).
  • The environment variable from environment files (.env.dev, .env.preprod, and .env.prod) has the second priority.
  • The configuration file (./configs/config.yaml) has the lowest priority (default values).

APP_ENV is use to determine the environment. It is set on the Makefile when running the commands (make dev, make preprod, and make prod.)

Tests

Always set the APP_ENV in IDE before running the tests.

Supabase Local Deployment (Self-Hosting)

WARNING: The local deployment of Supabase and the one use in the production in Supabase website have different versions. Check the SELECT version(). Make sure that the migrations, schemas, and queries are compatible.

See Self-Hosting with Docker guide.

  1. Get the code and clone it on different directory outside of this project:

git clone --branch master --depth 1 https://github.com/supabase/supabase

  1. Copy the docker compose files over to the ./deployment/docker/_supabase/ directory in this project:
cp -rf supabase/docker/* meissa/deployment/docker/_supabase/
  1. Copy the fake env vars:
cp supabase/docker/.env.example meissa/deployment/docker/_supabase/.env
  1. To generate and apply all secrets at once, go to the project ./deployment/docker/_supabase/ directory and run the following command and see the self-hosting docker for other guides:
sh ./utils/generate-keys.sh
  1. Switch to the project root:
cd meissa
  1. Compose up the docker files use make command:
make supabase-compose up

Updating and Uninstalling

For updating and uninstalling, see the self-hosting docker guide.

Make sure to read the guide for any new instructions. NOTE: The guide removes the data, so be careful.

But this is my usual way of doing it:

  1. Stop and remove existing containers, images, networks, and volumes related to previous Supabase deployment:
make supabase-compose down
  1. Remove the existing Supabase docker compose files in the project:
sudo rm -rf ./deployment/docker/_supabase/*
  1. Clone the latest Supabase code outside this project again:
git clone --branch master --depth 1 https://github.com/supabase/supabase
  1. Copy the docker compose files over to the ./deployment/docker/_supabase/ directory in this project again as well as the fake env vars:
cp -rf supabase/docker/* meissa/deployment/docker/_supabase/
cp supabase/docker/.env.example meissa/deployment/docker/_supabase/.env
  1. Switch to the project root:
cd meissa
  1. Run the scripts related to generating and applying all secrets:

Sometimes new scripts are added to the Supabase repo. So make sure to read the self-hosting docker guide or the repo itself if there are any new instructions.

Scripts are usually in the ./_supabase/docker/utils directory.

  1. Compose up the docker files use make command:
make supabase-compose up

Connecting to the database

Supabase

Most Supabase "direct connection" hosts now resolve to IPv6 only. If the local machine or CI/CD environment (like some GitHub Actions runners) doesn't have IPv6 enabled, the connection will time out before it even reaches Supabase.

Solution: Use the "transaction pooler" or "session pooler" URL instead of the direct one, as the pooler host usually supports IPv4.

There are multiple ways to connect to the database:

  1. Data API (REST & GraphQL) - This is the most common way to interact with Supabase from a frontend or mobile app. You don't use a "connection string" here; instead, you use the Supabase client SDK.
  2. Postgres Client Connections:
    • Direct Connection (port 5432)
      • It talks directly to the Postgres engine with no middleman.
      • Most Supabase projects now use IPv6 by default for direct connections. If the local ISP or the server ( like older AWS EC2s) only supports IPv4, this connection will fail unless you pay for the "IPv4 Add-on".
      • Best for long-lived servers (like a Docker container on Fly.io or a dedicated VM) that need the absolute lowest latency and full control. This app also uses this connection using some of the Go packages.
    • Session Pooler (port 5432 via Supavisor)
      • It assigns one database "slot" to your app and keeps it there until you disconnect. It supports both IPv4 and IPv6, making it the "fix" for connection issues.
      • It supports all Postgres features (advisory locks, temp tables, SET commands).
      • Best for migrations (like Goose), GoLand database plugin, administrative scripts, and traditional backends that need IPv4 support.
    • Transaction Pooler (port 6543 via Supavisor)
      • This is the "high-occupancy lane". It is built for a massive scale.
      • It doesn't give you a dedicated connection. Instead, it "borrows" a connection from the pool only while a query is actually running. As soon as the transaction is done, the connection is given to someone else.
      • It breaks session-mode features. You cannot use temporary tables or advisory locks here because the next query you run might be on a completely different internal connection.
      • Best for serveless functions (AWS Lambda, Vercel, Netlify) where you might have 1,000 functions waking up the exact same time.

The GoLand Database plugin uses the session pooler connection with the SSL certificate that can be downloaded from Supabase dashboard.

References:

PostgreSQL v18

Currently the Supabase version is v17.6 (see SELECT version();).

Here are a few things to consider when migrating to PostgreSQL v18.

UUIDv7

Currently, the UUIDv7 (uuidv7()) is not supported by PostgreSQL v17 and below. The app uses custom function uuid_generate_v7() to generate UUIDv7 on tables id column (see the 20260325113653_init.sql init migration file).

To replace uuid_generate_v7() with uuidv7() on PostgreSQL v18 and above, create a new migration file in this way:

-- +goose StatementBegin
-- 1. Remove the old default from the table
ALTER TABLE <table_name> ALTER COLUMN id DROP DEFAULT;

-- 2. (Optional) Add a new default if you are switching to native uuidv7()
-- ALTER TABLE <table_name> ALTER COLUMN id SET DEFAULT uuidv7();

-- 3. Now it is safe to drop the function without CASCADE
DROP FUNCTION IF EXISTS uuid_generate_v7();
-- +goose StatementEnd

Security: Generating JWT Secrets

JWT secrets must be long, random, and cryptographically secure. To generate a standard 32-byte (256-bit) hex-encoded secret, run:

openssl rand -hex 32

Add the output to the .env-* files as JWT_SECRET.

Frontend

pnpm

This project uses pnpm workspace, and it contains multiple packages (e.g., @meissa/web, @meissa/api-client, etc.).

Do not confuse workspace packages with dependencies (e.g, react, react-relay, typescript, etc.) do you see in package.json depencies.

--filter flag

This flag is used to execute commands to a specific package.

For example, to install a dependency to a specific package:

pnpm add <dependency> --filter <package>

# Example:
# pnpm add react --filter @meissa/web
# pnpm add -D typescript --filter @meissa/web

Adding a new package

Let's say we have an existing package named @meissa/web and we want to create a new package named @meissa/api-client.

Initialize the new package

mkdir -p packages/api-client # let's say the new package will be located in `packages` directory for example only
cd packages/api-client
pnpm init                    # this is necessary to have the `packageManager` field in package.json

Then edit the package.json and change the name to @meissa/api-client.

Add dependencies

cd first to the project root.

We can just simply add the dependency using pnpm add <dependency> --filter @meissa/api-client if we want the dependency to be installed for that package only.

But if we want to have the same version across all the packages in the workspace, follow the steps below:

Dependency is not listed to catalog in pnpm-workspace.yaml

We need to use the --save-catalog flag to add the dependency to the pnpm-workspace.yaml file's catalog.

This will add the dependency to the pnpm-workspace.yaml file's catalog and also to the @meissa/api-client package.json with version catalog:.

NOTE: You need to add the @version after the dependency name

pnpm add <dependency>@version --save-catalog --filter @meissa/api-client

To remove it from the @meissa/api-client package.json (but it will not be removed it from the pnpm-workspace.yaml file's catalog:

pnpm remove <dependency> --filter @meissa/api-client
Dependency is listed to catalog in pnpm-workspace.yaml

Since this is already listed in the pnpm-workspace.yaml file's catalog, we don't need to use the --save-catalog flag.

This will add the dependency to the @meissa/api-client package.json with version catalog:.

pnpm add <dependency> --filter @meissa/api-client

We can also explicitly tell to use catalog: as a version of the dependency (they are just the same):

pnpm add <dependency>@catalog: --filter @meissa/api-client

To remove it from the @meissa/api-client package.json (but it will not be remove it from the pnpm-workspace.yaml file's catalog:

pnpm remove <dependency> --filter @meissa/api-client
Linking (aka Workspace Linking, Internal Referencing)

To link a package to another package.

For example, to link @meissa/api-client to @meissa/web (this will add @meissa/api-client to the @meissa/web package.json with version workspace:*):

pnpm add @meissa/api-client --filter @meissa/web --workspace

To unlink or remove it from the @meissa/web package.json:

pnpm remove @meissa/api-client --filter @meissa/web

React

  • use useActionState for form related state management and component

Relay

  • always use preloaded queries (useQueryLoader and usePreloadedQuery) for fetching data instead of useLazyLoadQuery