auth,feeds,ai,ui,deploy: build Stellar

Use Effect services for requests, AI tasks, and the RSS worker.
Add admin accounts, feed sharing, and stored reading profiles.
Add Biome, Effect tools, and Podman deployment with Caddy.
This commit is contained in:
EFLKumo committed 2026-10-11 16:21:39 +08:00
1 parent ba8f925eed
commit ce87e56053
137 files changed
+14652 -2457

No files matched your search

+82
View File
@@ -0,0 +1,82 @@
# Architecture
## Terms
| Term | Meaning |
| --- | --- |
| Feed | A link or Markdown text stored in Stellar. |
| Feed state | Saved values for one user and one feed. |
| Profile | Stored preferences and learned reading interests for one user. |
| Public feed | A feed that other users can read. |
| RSS | Really Simple Syndication. An RSS source lists articles. |
| Effect | The library that controls dependencies, errors, tasks, and resource lifetimes. |
| Layer | An Effect value that constructs services and their dependencies. |
| Scope | An Effect resource lifetime. Resources close when the scope ends. |
| AI tool | A server function that the AI agent can request. |
## Program structure
React provides the user interface.
TanStack Start provides routes and the server build.
Shared shadcn components are in `packages/ui`.
The root React layout provides tooltips for the application.
Effect provides browser request control and server services.
React remains responsible for rendering and component state.
Server services use Effect dependency injection through layers.
Service dependencies have one implementation for each external system.
Drizzle stores application data in PostgreSQL.
Better Auth checks credentials and maintains sessions.
Only the administrator can create accounts.
The server checks the session for each protected request.
The Vercel AI SDK provides model access, streaming chat, and AI tools.
Model configuration and provider keys stay on the server.
AI tools use the signed-in user's access permissions.
## Feed rules
A new feed belongs to its author.
Forwarding a feed makes it public.
Other users can add comments to public feeds.
The server checks feed visibility before it returns content.
The server uses a normalized URL to find an existing public feed.
If a matching public feed exists, submission returns that feed.
The browser then opens the existing feed.
Markdown feeds do not use URL duplicate detection.
## AI and RSS
The AI can summarize feeds and translate their content.
The chat agent can read accessible feeds and operate on the user's subscriptions and profile.
The user profile remains in PostgreSQL.
Interest feedback changes the stored profile used for later article selection.
The worker retries unfinished profile updates after provider failures or interrupted requests.
The RSS worker runs separately from the web server.
It reads subscriptions and each user's preferences.
It asks the model to select articles and produce summaries.
It stores selected articles as AI feeds.
An RSS recommendation can reuse an existing public feed.
The user's feed state stores the AI summary and recommendation status.
The worker limits concurrent subscription checks with Effect.
## Lifetimes and interruption
Effect scopes own server resources.
Database connections close when their scope ends.
Request cancellation interrupts the corresponding Effect work.
AI requests receive the request's cancellation signal.
The worker uses the Effect Node runtime for startup and signal handling.
The runtime interrupts the worker before scoped resources close.
## Container structure
Compose starts the database, application, worker, and Caddy proxy.
The application and worker share one image.
Their entrypoint runs database setup before it starts the selected process.
PostgreSQL and Caddy use named volumes.
Only Caddy publishes host ports.
Caddy forwards application requests and supplies HTTPS for a public domain.
+81
View File
@@ -0,0 +1,81 @@
# Configuration
Use one line for each value in `.env`.
Use the names in `.env.example`.
Do not publish `.env` or provider keys.
## Site and account values
| Name | Purpose |
| --- | --- |
| `DOMAIN` | Host name that Caddy serves. Use `localhost` for a local deployment. |
| `APP_ORIGIN` | Complete site address. Use the public HTTPS address for a domain. |
| `HTTP_PORT` | Host port for HTTP. The local default is `8080`. |
| `HTTPS_PORT` | Host port for HTTPS and HTTP/3. The local default is `8443`. |
| `STELLAR_ADMIN_EMAIL` | Email address for the first administrator. |
| `STELLAR_ADMIN_USERNAME` | User name for the first administrator. |
| `STELLAR_ADMIN_PASSWORD` | Password for the first administrator. The script generates a value for an empty field. |
| `BETTER_AUTH_SECRET` | Authentication secret. The script generates a value for an empty field. |
| `POSTGRES_PASSWORD` | Database password. The script generates a value for an empty field. |
Use at least 12 characters for an administrator password.
Use at least 32 characters for an authentication secret.
Use URL-safe characters for a database password.
The script uses random hexadecimal characters for generated secrets.
Do not change a database password only by editing `.env` after the first deployment.
The setup program creates the first administrator.
It does not reset an existing account's password at each start.
Create and manage later accounts in the administration panel.
## AI models
`AI_MODELS` is a JSON array.
Each item has four fields.
| Field | Purpose |
| --- | --- |
| `id` | Unique ID that Stellar stores for the model. |
| `name` | Model name that users see. |
| `provider` | `google`, `openai`, or `anthropic`. |
| `model` | Model ID from the provider. |
Put the JSON array on one line in `.env`.
Use single quotes around the array.
```dotenv
AI_MODELS='[{"id":"gemini-flash","name":"Gemini Flash","provider":"google","model":"gemini-flash-latest"}]'
GOOGLE_GENERATIVE_AI_API_KEY=your-provider-key
```
The Gemini alias selects the provider's current Flash model.
Select a fixed model ID when you require a specific model version.
See the [Gemini model list](https://ai.google.dev/gemini-api/docs/models).
Set `OPENAI_API_KEY` for OpenAI models.
Set `ANTHROPIC_API_KEY` for Anthropic models.
Users can select only the models in `AI_MODELS`.
Provider keys remain on the server.
## Worker values
| Name | Purpose |
| --- | --- |
| `RSS_POLL_SECONDS` | Time between RSS checks. The default is `900` seconds. |
| `RSS_CONCURRENCY` | Maximum number of subscriptions checked together. The default is `2`. |
Change these values to control RSS request volume.
The worker uses each user's stored preferences to select articles.
## Direct server values
Compose supplies these values to the application and worker.
Set them manually when you run the server outside Compose.
| Name | Purpose |
| --- | --- |
| `DATABASE_URL` | PostgreSQL connection address. |
| `BETTER_AUTH_URL` | Public authentication address. Compose uses `APP_ORIGIN`. |
| `HOST` | Server bind address. The container uses `0.0.0.0`. |
| `PORT` | Server port. The container uses `3000`. |
| `NODE_ENV` | Runtime mode. The container uses `production`. |
+178
View File
@@ -0,0 +1,178 @@
# Deployment
## Terms
| Term | Meaning |
| --- | --- |
| Container | A process that runs with a separate file system and network. |
| Compose provider | A program that reads `compose.yaml` and starts containers. |
| Rootless Podman | Podman that runs without the root account. |
| DNS | Domain Name System. DNS connects a domain name to an IP address. |
| TLS | Transport Layer Security. TLS protects network connections. |
| Volume | Data storage that remains after a container stops. |
## Install the host tools
1. Install Podman with your operating system package manager.
2. Install `podman-compose`, Python 3, `python-dotenv`, curl, and Bash.
3. Confirm that Podman can run as your user.
```sh
podman info
podman-compose version
```
Podman uses an external Compose provider.
The deployment script selects `podman-compose` when it is installed.
The script uses `python-dotenv` to read and update `.env`.
This library is also a dependency of `podman-compose`.
To select another provider, set `PODMAN_COMPOSE_PROVIDER` to its executable path.
See the [Podman Compose instructions](https://docs.podman.io/en/latest/markdown/podman-compose.1.html).
On macOS or Windows, start a Podman machine first.
```sh
podman machine init
podman machine start
```
## Start a local deployment
1. Copy `.env.example` to `.env`.
2. Set a provider key for each provider in `AI_MODELS`.
3. Set the first administrator's name and email address.
4. Run the deployment script.
```sh
cp .env.example .env
./scripts/deploy.sh
```
The script generates a secret for each empty password or authentication field.
It sets `.env` permissions to allow access only by the file owner.
It builds the application image and starts four containers.
The containers run PostgreSQL, the application, the RSS worker, and Caddy.
The script checks the database, application, site, and worker before it reports success.
Open `http://localhost:8080`.
Sign in with `STELLAR_ADMIN_USERNAME` and `STELLAR_ADMIN_PASSWORD` from `.env`.
Use the administration panel to create other accounts.
The application does not provide public account registration.
## Connect a public domain
1. Set the domain's DNS records to the server's public IP address.
2. Permit incoming TCP traffic on ports 80 and 443.
3. Permit incoming UDP traffic on port 443 to use HTTP/3.
4. Set these values in `.env`.
```dotenv
DOMAIN=feeds.example.com
APP_ORIGIN=https://feeds.example.com
HTTP_PORT=80
HTTPS_PORT=443
```
5. Permit rootless processes to bind the ports, if the host requires this change.
```sh
printf '%s\n' 'net.ipv4.ip_unprivileged_port_start=80' | sudo tee /etc/sysctl.d/90-stellar.conf
sudo sysctl --system
```
This host setting lets unprivileged processes bind ports 80 and above.
You can instead forward public ports 80 and 443 to host ports 8080 and 8443.
Keep `HTTP_PORT=8080` and `HTTPS_PORT=8443` when you use port forwarding.
See the [Podman port instructions](https://github.com/podman-container-tools/podman/blob/main/troubleshooting.md#48-pasta-fails-with-listen-failed-or-rootless-podman-fails-with-bind-permission-denied).
6. Run `./scripts/deploy.sh`.
7. Open the address in `APP_ORIGIN`.
Caddy gets and renews the TLS certificate automatically.
The public domain must reach this server on ports 80 and 443.
See the [Caddy HTTPS instructions](https://caddyserver.com/docs/quick-starts/https).
Certificate data remains in the `stellar_caddy_data` volume.
## Keep containers after a restart
On a Linux host with systemd, enable the Podman restart service.
Then enable user services when the user is not signed in.
```sh
systemctl --user enable podman-restart.service
sudo loginctl enable-linger "$USER"
```
The Compose file uses the `unless-stopped` restart policy.
Current Podman versions restart these containers after a host restart.
See the [Podman restart policy instructions](https://docs.podman.io/en/latest/markdown/podman-update.1.html#restart-policy).
## Read logs and stop containers
Read container state and logs.
```sh
podman compose ps
podman compose logs --tail=100 app worker
podman compose logs --tail=100 proxy
```
Stop the deployment.
```sh
podman compose down
```
This command keeps the named volumes.
Do not add `--volumes` unless you intend to delete stored data.
## Update the application
1. Make a database backup.
2. Get the new application files.
3. Run `./scripts/deploy.sh`.
4. Check the site and the worker logs.
The application applies database changes before the server starts.
The worker uses the same application image.
## Make a database backup
Create a backup file outside the container.
Protect the backup file because it contains user data.
```sh
umask 077
mkdir -p backups
podman compose exec -T database pg_dump -U stellar -d stellar -Fc > backups/stellar.dump
```
Copy `.env` and the backup file to separate protected storage.
The `.env` file contains the authentication secret.
Keep this secret when you restore the database.
## Restore a database backup
This procedure replaces the database contents.
Stop the application and the worker before you restore the backup.
```sh
podman compose stop app worker
podman compose exec -T database pg_restore -U stellar -d stellar --clean --if-exists < backups/stellar.dump
./scripts/deploy.sh
```
## Correct deployment errors
| Error | Action |
| --- | --- |
| No Compose provider | Install `podman-compose`. Run `podman-compose version`. |
| Podman machine stopped | Run `podman machine start`. |
| Permission denied on a host port | Use ports 8080 and 8443, or complete the domain port procedure. |
| Database authentication failed | Keep the original `POSTGRES_PASSWORD` for an existing database volume. |
| Site health check failed | Check `APP_ORIGIN`, DNS, host ports, and proxy logs. |
| Certificate request failed | Confirm that public ports 80 and 443 reach Caddy. |
| AI request failed | Check the provider key, model ID, provider limits, and worker logs. |
The database uses `stellar_database`.
PostgreSQL does not change an existing volume's password when you edit `.env`.
+118
View File
@@ -0,0 +1,118 @@
# Development
## Prepare the workspace
Use Node.js 26 and pnpm 11.25.0.
Install the workspace packages from the lock file.
```sh
pnpm install --frozen-lockfile
```
Biome checks and formats the TypeScript, JavaScript, JSON, and CSS files.
The Effect development tools check Effect programs during type checks.
See the [Effect development tool instructions](https://effect.website/docs/v4/getting-started/devtools).
## Start the database
1. Copy `.env.example` to `.env`.
2. Set the authentication and administrator secrets.
3. Set the provider keys.
4. Start a separate development database.
```sh
podman run --detach --name stellar-dev-database \
--publish 127.0.0.1:5432:5432 \
--env POSTGRES_DB=stellar \
--env POSTGRES_USER=stellar \
--env POSTGRES_PASSWORD=stellar-development \
--volume stellar_dev_database:/var/lib/postgresql \
docker.io/library/postgres:18-alpine
```
5. Set the connection address and authentication address in `.env`.
```dotenv
DATABASE_URL=postgresql://stellar:stellar-development@127.0.0.1:5432/stellar
BETTER_AUTH_URL=http://localhost:3000
```
The development database publishes port 5432 only on the local host.
It uses a separate volume from the deployment database.
6. Run database setup with the environment file.
7. Start Vite with the same environment file.
```sh
cd apps/web
node --env-file=../../.env --import tsx src/server/setup.ts
node --env-file=../../.env node_modules/vite/bin/vite.js --port 3000
```
Node.js reads `.env` before it starts setup or Vite.
Run the workspace commands below from the workspace root.
## Run workspace commands
| Command | Purpose |
| --- | --- |
| `pnpm dev` | Start the development server with environment variables already set. |
| `pnpm build` | Build the server and browser files. |
| `pnpm typecheck` | Generate routes and check types. |
| `pnpm lint` | Check files with Biome. |
| `pnpm format` | Format files and apply Biome fixes. |
The production server starts from `apps/web/.output/server/index.mjs`.
The container starts database setup before it starts the server or worker.
## Edit user interface components
Add shared components to `packages/ui`.
Use the shadcn command from the workspace root.
```sh
pnpm dlx shadcn@latest add button -c apps/web
```
Import shared components from `@workspace/ui/components`.
The root layout must include `TooltipProvider`.
## Change version history
Use Jujutsu (`jj`) for version history.
Use scoped change descriptions.
```sh
jj status
jj describe -m 'feeds,ui: add the feed detail view'
```
## Run tests
Run the content tests with this command.
Database tests stay disabled until you set their connection values.
```sh
pnpm test
```
Use a separate test database.
Run database setup before you start the test server.
Set `TEST_DATABASE_URL` to that database to test AI and RSS operations.
These tests replace provider calls with test responses.
They do not use a paid provider.
Set `TEST_BASE_URL` to the test server address to test authentication and feed access.
Set `DATABASE_URL` to the test database.
Set `STELLAR_ADMIN_USERNAME` and `STELLAR_ADMIN_PASSWORD` to the test administrator's credentials.
The tests create temporary accounts and remove them at the end.
Use Playwright to test the browser interface.
Install its Chromium browser first.
Set `TEST_APP_ORIGIN` to the test server address.
Set `TEST_ADMIN_EMAIL` and `TEST_ADMIN_PASSWORD` to the test administrator's credentials.
```sh
pnpm exec playwright install chromium
pnpm test:e2e
```