+18
.env.template
+18
.env.template
···
1
+
#Aqua setup
2
+
NODE_ENV=development
3
+
PORT=3000
4
+
HOST=0.0.0.0
5
+
PUBLIC_URL=A publicly accessible url for aqua
6
+
DB_USER=postgres
7
+
DB_PASSWORD=supersecurepassword123987
8
+
DB_NAME=teal
9
+
DATABASE_URL="postgresql://${DB_USER}:${DB_PASSWORD}@localhost:5432/${DB_NAME}"
10
+
DOCKER_DB_URL="postgresql://${DB_USER}:${DB_PASSWORD}@host.docker.internal:5432/${DB_NAME}"
11
+
#This is not currently being used fully so can just use this default pubkey for now
12
+
DID_WEB_PUBKEY=zQ3sheEnMKhEK87PSu4P2mjAevViqHcjKmgxBWsDQPjLRM9wP
13
+
CLIENT_ADDRESS=A publicly accessible host for amethyst like amethyst.teal.fm
14
+
PUBLIC_DID_WEB=did:web:{aqua's PUBLIC_URL goes here after did:web:}
15
+
16
+
#amethyst
17
+
EXPO_PUBLIC_DID_WEB=same as PUBLIC_DID_WEB
18
+
EXPO_PUBLIC_BASE_URL=same as CLIENT_ADDRESS but with http scheme like https://amethyst.teal.fm
+2
.gitignore
+2
.gitignore
+28
README.md
+28
README.md
···
23
23
```
24
24
25
25
Open http://localhost:3000/ with your browser to see the home page. You will need to login with Bluesky to test the posting functionality of the app. Note: if the redirect back to the app after you login isn't working correctly, you may need to replace the `127.0.0.1` with `localhost`.
26
+
27
+
28
+
### Running the full stack in docker for development
29
+
_Still a work in progress and may have some hiccups_
30
+
31
+
#### 1. Set up publicly accessible endpoints.
32
+
It is recommended if you are running aqua to make a publicly accessible endpoint for the app to post to. You can do that a couple of ways:
33
+
34
+
* Set up the traditional port forward on your router
35
+
* Use a tool like [ngrok](https://ngrok.com/) with the command `ngrok http 8080` or [Cloudflare tunnels](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/get-started/create-remote-tunnel/) (follow the 2a. portion of the guide when you get to that point).
36
+
37
+
If you do the cloudflare tunnels for amethyst as well,
38
+
you will also need
39
+
to follow [this](https://caddy.community/t/caddy-with-cloudflare-tunnel/18569) for routing to work properly.
40
+
Although you can just run it locally and it will work.
41
+
42
+
#### 2. Set up .envs
43
+
1. copy [.env.template](.env.template) and name it [.env](.env). The docker build will pull everything from this `.env`. There are notes in the [.env.template](.env.template) on what some of the values should be.
44
+
2. Follow the instructions in [piper](https://github.com/teal-fm/piper) to set up environment variables for the music scraper. But name it `.env.air`
45
+
46
+
#### 3. Run docker
47
+
1. Make sure docker and docker compose is installed
48
+
2. It is recommended to run 'docker compose -f compose.dev.yml pull' to pull the latest remote images before running the docker compose command.
49
+
3. Run `docker compose -f compose.dev.yml up -d`
50
+
51
+
52
+
And that's it! You should have the full teal.fm stack running locally. Now if you are working on aqua you can do `docker container stop aqua-app` and run that locally during development while everything else is running in docker.
53
+
+1
apps/amethyst/.env.template
+1
apps/amethyst/.env.template
···
1
+
EXPO_PUBLIC_DID_WEB=did:web:your.publicly-accessible-aqua-domain.com
+5
apps/amethyst/Caddyfile
+5
apps/amethyst/Caddyfile
+53
apps/amethyst/Dockerfile
+53
apps/amethyst/Dockerfile
···
1
+
FROM node:22-slim AS builder
2
+
ARG CLIENT_ADDRESS
3
+
4
+
ENV PNPM_HOME="/pnpm"
5
+
ENV PATH="$PNPM_HOME:$PATH"
6
+
RUN corepack enable
7
+
# Set working directory
8
+
WORKDIR /app
9
+
10
+
# Copy root workspace files
11
+
COPY package.json pnpm-workspace.yaml pnpm-lock.yaml ./
12
+
13
+
# Copy turbo.json
14
+
COPY turbo.json ./
15
+
16
+
# Copy workspace packages
17
+
COPY packages/db/ ./packages/db/
18
+
COPY packages/lexicons/ ./packages/lexicons/
19
+
COPY packages/tsconfig/ ./packages/tsconfig/
20
+
21
+
# Copy the aqua app
22
+
COPY apps/amethyst/ ./apps/amethyst/
23
+
24
+
# Copy .env
25
+
COPY ../../.env ./apps/amethyst/.env
26
+
27
+
# Build the aqua app
28
+
WORKDIR /app/apps/amethyst
29
+
RUN pnpm install
30
+
RUN pnpm run build:web
31
+
32
+
#create the client-json
33
+
RUN echo '{ \
34
+
"redirect_uris": ["https://'"${CLIENT_ADDRESS}"'/auth/callback"], \
35
+
"response_types": ["code"], \
36
+
"grant_types": ["authorization_code", "refresh_token"], \
37
+
"scope": "atproto transition:generic", \
38
+
"token_endpoint_auth_method": "none", \
39
+
"application_type": "web", \
40
+
"client_id": "https://'"${CLIENT_ADDRESS}"'/client-metadata.json", \
41
+
"client_name": "teal", \
42
+
"client_uri": "https://'"${CLIENT_ADDRESS}"'", \
43
+
"dpop_bound_access_tokens": true \
44
+
}' > /app/client-metadata.json \
45
+
46
+
47
+
FROM caddy:2.1.0-alpine AS caddy
48
+
EXPOSE 80
49
+
EXPOSE 443
50
+
EXPOSE 443/udp
51
+
COPY /apps/amethyst/Caddyfile /etc/caddy/Caddyfile
52
+
COPY --from=builder /app/apps/amethyst/build /srv
53
+
COPY --from=builder /app/client-metadata.json /srv/client-metadata.json
+1
apps/amethyst/package.json
+1
apps/amethyst/package.json
···
9
9
"ios": "expo run:ios",
10
10
"web": "expo start --web",
11
11
"build": "expo export --output-dir ./build --platform all",
12
+
"build:web": "expo export --output-dir ./build --platform web --clear",
12
13
"test": "jest --watchAll",
13
14
"lexgen": "lex gen-server ./lexicons/generated/server/ ./lexicons/src/"
14
15
},
+6
apps/amethyst/readme.md
+6
apps/amethyst/readme.md
···
10
10
11
11
## Development
12
12
13
+
Copy the [.env.template](.env.template) and name it `.env`
14
+
15
+
set `EXPO_PUBLIC_DID_WEB` to the domain aqua is running on.
16
+
For example if it's running at `aqua.teal.fm` it would be
17
+
`EXPO_PUBLIC_DID_WEB=did:web:aqua.teal.fm`. This is how amethyst sends requests to the appview.
18
+
13
19
TODO
-1
apps/amethyst/stores/authenticationSlice.tsx
-1
apps/amethyst/stores/authenticationSlice.tsx
···
44
44
) => {
45
45
// check if we have CF_PAGES_URL set. if not, use localhost
46
46
const baseUrl = process.env.EXPO_PUBLIC_BASE_URL || 'http://localhost:8081';
47
-
console.log('Using base URL:', baseUrl);
48
47
const initialAuth = createOAuthClient(baseUrl, 'bsky.social');
49
48
50
49
console.log('Auth client created!');
+1
-1
apps/amethyst/stores/preferenceSlice.tsx
+1
-1
apps/amethyst/stores/preferenceSlice.tsx
+11
-13
apps/aqua/Dockerfile
+11
-13
apps/aqua/Dockerfile
···
1
-
# Use the official Bun image as base
2
-
FROM oven/bun:1.0 AS builder
3
-
1
+
FROM node:22-slim AS builder
2
+
ENV PNPM_HOME="/pnpm"
3
+
ENV PATH="$PNPM_HOME:$PATH"
4
+
RUN corepack enable
4
5
# Set working directory
5
6
WORKDIR /app
6
7
7
8
# Copy root workspace files
8
-
COPY package.json bun.lockb ./
9
+
COPY package.json pnpm-workspace.yaml pnpm-lock.yaml ./
9
10
10
11
# Copy turbo.json
11
12
COPY turbo.json ./
···
18
19
# Copy the aqua app
19
20
COPY apps/aqua/ ./apps/aqua/
20
21
21
-
# Install dependencies
22
-
RUN bun install
23
-
24
-
# Build workspace packages (if needed)
25
-
RUN bun run build --filter=@teal/db
26
-
RUN bun run build --filter=@teal/lexicons
27
-
28
22
# Build the aqua app
29
23
WORKDIR /app/apps/aqua
30
-
RUN bun run build
24
+
RUN pnpm install
25
+
RUN pnpm run build
31
26
32
27
# Start production image (node lts ideally)
33
28
FROM node:bookworm-slim
···
41
36
COPY --from=builder /app/apps/aqua/node_modules ./node_modules
42
37
COPY --from=builder /app/apps/aqua/package.json ./
43
38
39
+
# install pino-pretty for logs
40
+
RUN npm install -g pino-pretty@13.0.0
41
+
44
42
# move public to cwd
45
43
RUN mv ./dist/public ./public
46
44
···
51
49
EXPOSE 3000
52
50
53
51
# Start the application
54
-
CMD ["npm", "run", "start"]
52
+
CMD ["node", "dist/index.cjs", "|", "pino-pretty"]
+6
-5
apps/aqua/package.json
+6
-5
apps/aqua/package.json
···
20
20
"@atproto/syntax": "^0.3.0",
21
21
"@atproto/xrpc-server": "^0.7.4",
22
22
"@braintree/sanitize-url": "^7.1.0",
23
-
"drizzle-orm": "^0.38.3",
24
23
"@hono/node-server": "^1.13.7",
25
24
"@libsql/client": "^0.14.0",
25
+
"@teal/db": "workspace:*",
26
+
"@teal/lexicons": "workspace:*",
26
27
"dotenv": "^16.4.5",
28
+
"dotenv-expand": "^12.0.2",
29
+
"drizzle-orm": "^0.38.3",
27
30
"envalid": "^8.0.0",
28
31
"hono": "^4.6.9",
29
32
"jose": "^5.9.6",
30
33
"pino": "^9.5.0",
31
34
"turbo": "^2.2.3",
32
-
"uhtml": "^4.5.11",
33
-
"@teal/db": "workspace:*",
34
-
"@teal/lexicons": "workspace:*"
35
+
"uhtml": "^4.5.11"
35
36
},
36
37
"devDependencies": {
37
-
"@teal/tsconfig": "workspace:*",
38
38
"@atproto/lex-cli": "^0.5.4",
39
+
"@teal/tsconfig": "workspace:*",
39
40
"@types/node": "^20.17.6",
40
41
"drizzle-kit": "^0.30.1",
41
42
"pino-pretty": "^13.0.0",
+1
-1
apps/aqua/src/index.ts
+1
-1
apps/aqua/src/index.ts
+3
-2
apps/aqua/src/lib/env.ts
+3
-2
apps/aqua/src/lib/env.ts
···
1
1
import dotenv from 'dotenv';
2
+
import dotenvExpand from "dotenv-expand";
2
3
import { cleanEnv, host, port, str, testOnly } from 'envalid';
3
4
import process from 'node:process';
4
5
5
-
dotenv.config();
6
+
dotenvExpand.expand(dotenv.config());
6
7
// in case our .env file is in the root folder
7
-
dotenv.config({ path: './../../.env' });
8
+
dotenvExpand.expand(dotenv.config({ path: './../../.env' }));
8
9
9
10
export const env = cleanEnv(process.env, {
10
11
NODE_ENV: str({
+76
-46
compose.dev.yml
+76
-46
compose.dev.yml
···
6
6
container_name: aqua-app
7
7
ports:
8
8
- "3000:3000"
9
+
extra_hosts:
10
+
- "host.docker.internal:host-gateway"
9
11
networks:
10
12
- app_network
11
13
depends_on:
12
14
- postgres
13
-
- redis
14
15
env_file:
15
16
- .env
16
-
# traefik:
17
-
# image: traefik:v2.10
18
-
# container_name: traefik
19
-
# command:
20
-
# - "--api.insecure=true"
21
-
# - "--providers.file.directory=/etc/traefik/dynamic"
22
-
# - "--providers.file.watch=true"
23
-
# - "--entrypoints.web.address=:80"
24
-
# ports:
25
-
# - "80:80" # HTTP
26
-
# - "8080:8080" # Dashboard
27
-
# volumes:
28
-
# - ./traefik/dynamic:/etc/traefik/dynamic:ro
29
-
# networks:
30
-
# - app_network
31
-
# extra_hosts:
32
-
# - "host.docker.internal:host-gateway" # This allows reaching host machine
33
-
34
-
# postgres:
35
-
# image: postgres:latest
36
-
# container_name: postgres_db
37
-
# environment:
38
-
# POSTGRES_USER: postgres
39
-
# POSTGRES_PASSWORD: yourpassword
40
-
# POSTGRES_DB: yourdatabase
41
-
# ports:
42
-
# - "5432:5432"
43
-
# volumes:
44
-
# - postgres_data:/var/lib/postgresql/data
45
-
# networks:
46
-
# - app_network
47
-
48
-
# redis:
49
-
# image: redis:latest
50
-
# container_name: redis_cache
51
-
# ports:
52
-
# - "6379:6379"
53
-
# volumes:
54
-
# - redis_data:/data
55
-
# command: redis-server --appendonly yes
56
-
# networks:
57
-
# - app_network
58
-
17
+
environment:
18
+
DATABASE_URL: ${DOCKER_DB_URL}
19
+
amethyst:
20
+
build:
21
+
context: .
22
+
dockerfile: apps/amethyst/Dockerfile
23
+
args:
24
+
- CLIENT_ADDRESS=${CLIENT_ADDRESS}
25
+
ports:
26
+
- "80:80"
27
+
- "443:443"
28
+
- "443:443/udp"
29
+
volumes:
30
+
- caddy_data:/data
31
+
- caddy_config:/config
32
+
networks:
33
+
- app_network
34
+
environment:
35
+
CLIENT_ADDRESS: ${CLIENT_ADDRESS}
36
+
postgres:
37
+
image: postgres:latest
38
+
container_name: postgres_db
39
+
environment:
40
+
POSTGRES_USER: ${DB_USER}
41
+
POSTGRES_PASSWORD: ${DB_PASSWORD}
42
+
POSTGRES_DB: ${DB_NAME}
43
+
ports:
44
+
- "5432:5432"
45
+
volumes:
46
+
- postgres_data:/var/lib/postgresql/data
47
+
extra_hosts:
48
+
- "host.docker.internal:host-gateway"
49
+
networks:
50
+
- app_network
51
+
cadet:
52
+
image: ghcr.io/espeon/cadet
53
+
ports:
54
+
- "9000:9000"
55
+
env_file:
56
+
- .env
57
+
environment:
58
+
DATABASE_URL: ${DOCKER_DB_URL}
59
+
extra_hosts:
60
+
- "host.docker.internal:host-gateway"
61
+
networks:
62
+
- app_network
63
+
depends_on:
64
+
- postgres
65
+
satellite:
66
+
image: ghcr.io/espeon/satellite
67
+
ports:
68
+
- "3132:3000"
69
+
env_file:
70
+
- .env
71
+
environment:
72
+
DATABASE_URL: ${DOCKER_DB_URL}
73
+
extra_hosts:
74
+
- "host.docker.internal:host-gateway"
75
+
networks:
76
+
- app_network
77
+
depends_on:
78
+
- postgres
79
+
piper:
80
+
image: ghcr.io/teal-fm/piper:main
81
+
# Depends on your .env.air
82
+
ports:
83
+
- "8080:8080"
84
+
env_file:
85
+
- .env.air
86
+
volumes:
87
+
- piper_data:/db
59
88
networks:
60
89
app_network:
61
90
driver: bridge
62
-
63
91
volumes:
64
92
postgres_data:
65
-
redis_data:
93
+
caddy_data:
94
+
caddy_config:
95
+
piper_data:
+2
-1
package.json
+2
-1
package.json
···
9
9
"typecheck": "pnpm -r exec tsc --noEmit",
10
10
"fix": "biome lint --apply . && biome format --write . && biome check . --apply",
11
11
"nuke": "rimraf node_modules */*/node_modules",
12
-
"lex:gen-server": "turbo lex:gen-server"
12
+
"lex:gen-server": "turbo lex:gen-server",
13
+
"db:migrate": "cd ./packages/db && drizzle-kit migrate"
13
14
},
14
15
"dependencies": {
15
16
"@atproto/oauth-client": "^0.3.8"
+9
packages/db/drizzle.config.ts
+9
packages/db/drizzle.config.ts
···
1
1
import { defineConfig } from "drizzle-kit";
2
+
import dotenv from "dotenv";
3
+
import dotenvExpand from "dotenv-expand";
4
+
5
+
//Loads from root .env
6
+
dotenvExpand.expand(dotenv.config({ path: '../../.env' }));
7
+
//Or can be overridden by .env in the current folder
8
+
dotenvExpand.expand(dotenv.config());
9
+
10
+
2
11
3
12
export default defineConfig({
4
13
dialect: "postgresql",
+2
-1
packages/db/package.json
+2
-1
packages/db/package.json
+14
pnpm-lock.yaml
+14
pnpm-lock.yaml
···
265
265
dotenv:
266
266
specifier: ^16.4.5
267
267
version: 16.4.7
268
+
dotenv-expand:
269
+
specifier: ^12.0.2
270
+
version: 12.0.2
268
271
drizzle-orm:
269
272
specifier: ^0.38.3
270
273
version: 0.38.4(@libsql/client@0.14.0)(@types/react@19.0.14)(expo-sqlite@15.2.9(expo@53.0.9(@babel/core@7.26.0)(@expo/metro-runtime@5.0.4(react-native@0.79.2(@babel/core@7.26.0)(@types/react@19.0.14)(react@19.0.0)))(react-native@0.79.2(@babel/core@7.26.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0))(react-native@0.79.2(@babel/core@7.26.0)(@types/react@19.0.14)(react@19.0.0))(react@19.0.0))(postgres@3.4.5)(react@19.0.0)
···
336
339
'@types/node':
337
340
specifier: ^20.17.6
338
341
version: 20.17.14
342
+
dotenv-expand:
343
+
specifier: ^12.0.2
344
+
version: 12.0.2
339
345
340
346
packages/lexicons:
341
347
dependencies:
···
3686
3692
3687
3693
dotenv-expand@11.0.7:
3688
3694
resolution: {integrity: sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==}
3695
+
engines: {node: '>=12'}
3696
+
3697
+
dotenv-expand@12.0.2:
3698
+
resolution: {integrity: sha512-lXpXz2ZE1cea1gL4sz2Ipj8y4PiVjytYr3Ij0SWoms1PGxIv7m2CRKuRuCRtHdVuvM/hNJPMxt5PbhboNC4dPQ==}
3689
3699
engines: {node: '>=12'}
3690
3700
3691
3701
dotenv@16.4.7:
···
11013
11023
domhandler: 5.0.3
11014
11024
11015
11025
dotenv-expand@11.0.7:
11026
+
dependencies:
11027
+
dotenv: 16.4.7
11028
+
11029
+
dotenv-expand@12.0.2:
11016
11030
dependencies:
11017
11031
dotenv: 16.4.7
11018
11032