Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
3e322f4
Add canonical unified ingestion proof and CI gate
Apr 6, 2026
d72df57
chore: checkpoint canonical unified proof and feed updates
Apr 9, 2026
3d22f88
fix: make local compose health and ports self-healing
Apr 9, 2026
46ab077
feat: protocol bridge integration, AT bridge webhook, i18n, profileSt…
Apr 18, 2026
a5c5660
chore: sync in-progress API and frontend updates
Apr 19, 2026
f7633c9
wip: save current state
Apr 19, 2026
07ed95a
wip: save current state - April 20
Apr 20, 2026
a70ea02
feat(canonical): align hashtag and protocol-bridge implementation
Apr 20, 2026
05a6733
Add AT bridge and feed updates
Apr 21, 2026
36cd99c
chore: canonical proof gate - AT/AP bridge ingestion, schema migratio…
Apr 22, 2026
c27b608
feat(memory): sprint 1+2 — feed metrics, carousel, AP quote ingestion…
May 2, 2026
3519973
Add keyword rules management panel to federation dashboard
May 2, 2026
eea92fc
feat(memory): use delegated app moderation endpoints for pod policy sync
May 2, 2026
ac1d558
feat(chat): AP DM persistence via Solid Notifications webhook
May 2, 2026
96f8dbb
test(chat): cover transient Solid Notification DM persistence
May 2, 2026
4e2719b
feat: add mastodon profile discovery and dm moderation surfaces
May 2, 2026
d63cc7d
feat: add OG link previews and improve mention autocomplete
May 2, 2026
8a39493
feat: support mixed media carousels in feed and messages
May 2, 2026
3ab1ab1
fix: support full same-type carousels and harden Klipy GIF config
May 2, 2026
715d7c6
chore: document and harden Klipy env configuration
May 2, 2026
232d768
fix: align Klipy client with API docs
May 2, 2026
1398b81
perf: add feed keyset cursoring and local cache retention
May 2, 2026
d231bba
feat: cache AT/AP actor profiles and persist follow state
May 2, 2026
2b80808
feat: social graph counts, notification read state, bookmarks, viewer…
May 2, 2026
1b7bd0c
feat: add mastodon-style grouped notifications and read controls
May 3, 2026
dde8381
feat: add appearance controls for notification grouping behavior
May 3, 2026
d213a56
feat: notifications+feed - reply/quote kinds, following feed, AT noti…
May 3, 2026
dccf566
revert: remove GET /at/notifications route
May 3, 2026
a4b9d46
fix(auth): link AT DID in legacy signin/signup flows
May 4, 2026
0f5e6c1
fix: make chat commits pod-authoritative
May 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
The diff you're trying to view is too large. We only load the first 3000 changed files.
98 changes: 98 additions & 0 deletions .github/workflows/canonical-unified-proof.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
name: Canonical Unified Proof

on:
workflow_dispatch:
pull_request:
paths:
- "api/**"
- ".github/workflows/canonical-unified-proof.yml"
push:
branches:
- main
paths:
- "api/**"
- ".github/workflows/canonical-unified-proof.yml"

concurrency:
group: canonical-unified-proof-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: read

jobs:
proof:
runs-on: ubuntu-latest
timeout-minutes: 20

services:
postgres:
image: postgres:16
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: localpassword
POSTGRES_DB: memory
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U postgres -d memory"
--health-interval 10s
--health-timeout 5s
--health-retries 10

env:
DB_URL: postgres://postgres:localpassword@localhost:5432/memory
API_PORT: 8794
FIREHOSE_BRIDGE_SECRET: ci-canonical-bridge-secret
JWT_SECRET: ci-jwt-secret
MEMORY_API_BASE_URL: http://localhost:8794
MEMORY_PROOF_DB_VERIFY: "true"

steps:
- name: Check Out Repository
uses: actions/checkout@v4

- name: Set Up Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.2.15

- name: Install API Dependencies
run: bun install --cwd api --frozen-lockfile

- name: Apply Database Schema
run: bun --cwd api drizzle:push

- name: Start Memory API
run: |
bun --cwd api src/index.ts > /tmp/memory-api.log 2>&1 &
echo $! > /tmp/memory-api.pid

- name: Wait For API Health
run: |
for i in $(seq 1 40); do
if curl -fsS "${MEMORY_API_BASE_URL}/health" >/dev/null; then
exit 0
fi
sleep 1
done
echo "Memory API failed to become healthy"
cat /tmp/memory-api.log || true
exit 1

- name: Run Canonical Unified Proof
run: bun --cwd api proof:canonical:unified

- name: Upload API Log On Failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: memory-api-log
path: /tmp/memory-api.log

- name: Stop Memory API
if: always()
run: |
if [ -f /tmp/memory-api.pid ]; then
kill "$(cat /tmp/memory-api.pid)" >/dev/null 2>&1 || true
fi
8 changes: 7 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
{
"eslint.workingDirectories": ["./vueFrontend"]
"eslint.workingDirectories": ["./vueFrontend"],
"python.terminal.useEnvFile": true,
"files.associations": {
"**/api/drizzle/*.sql": "plaintext",
"**/drizzle/*.sql": "plaintext"
},
"mssql.intelliSense.enableErrorChecking": false
}
26 changes: 25 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
.DEFAULT_GOAL := help
.PHONY: docker-build docker-up build start log stop restart
.PHONY: docker-build docker-up build start log stop restart db-push db-generate api-typecheck

DOCKER_COMPOSE_DEV=docker compose -f docker-compose-dev.yml --env-file .env
DOCKER_COMPOSE_PROD=docker compose -f docker-compose-prod.yml --env-file ./api/.env --env-file ./frontend/.env
Expand Down Expand Up @@ -83,3 +83,27 @@ publish-backend-latest:
export TAG=latest
$(DOCKER_COMPOSE_PROD) build app-backend
$(DOCKER_COMPOSE_PROD) push app-backend

# ---------------------------------------------------------------------------
# Database helpers
# Requires the dev containers (make start) to be running so the 'pg' service
# is accepting connections on localhost:5432.
# ---------------------------------------------------------------------------

# Push schema changes directly to the dev DB (fast, for local development).
# Equivalent to: "apply whatever the Drizzle schema files describe right now".
db-push:
cd api && DB_URL=postgres://postgres:$(POSTGRES_PASSWORD)@localhost:5432/postgres bun run drizzle:push

# Generate a checked-in Drizzle migration SQL file from the current schema.
# Commit the generated file in api/drizzle/ before deploying to production.
db-generate:
cd api && bun run drizzle:generate

# Apply all pending checked-in migration files (use in CI / production).
db-migrate:
cd api && DB_URL=postgres://postgres:$(POSTGRES_PASSWORD)@localhost:5432/postgres bun run drizzle:migrate

# Type-check Memory API TypeScript without emitting build artifacts.
api-typecheck:
cd api && bun run typecheck
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Memory

A Microblog-App that saves all data on your Pod.
A Responsive Microblog-App and PWA that saves all data on your Pod.

Built on the [ActivityPods](https://activitypods.org) framework.

Expand Down Expand Up @@ -38,10 +38,19 @@ Currently optimised mobile devices are:
- go into the directory `cd api`
- run `bun install` to install the dependencies
- run `bun dev` to start the api
- run `bun run typecheck` to perform a dedicated TypeScript type-check for the API

From the repository root, you can also run:

- `bun run typecheck:api` to type-check the API
- `bun run check:api` to run API type-check + API tests
- `make api-typecheck` for a Makefile-based type-check entrypoint

#### Running the frontend
- go into the directory `cd frontend`
- run `bun install` to install the dependencies
- copy env template: `cp .env.example .env`
- set `VITE_KLIPY_APP_KEY` in `frontend/.env` to enable Klipy GIF search/trending/recent
- run `bun dev` to start the frontend

### Building the app for android
Expand Down
25 changes: 23 additions & 2 deletions api/.env.example
Original file line number Diff line number Diff line change
@@ -1,4 +1,25 @@
DB_URL=postgres://postgres:postgres@pg:5432/postgres
DB_URL=postgres://postgres:localpassword@localhost:5432/memory
JWT_SECRET=SuperSecretJWTSecret
API_PORT=8794
FRONTEND_URL=http://localhost:4000
API_URL=http://localhost:8794
FRONTEND_URL=http://localhost:4000

# Optional: when true, Memory may remove and re-register app grants if
# upgradeNeeded remains stuck and no active inbox webhook exists.
ACTIVITYPODS_ENABLE_REGISTRATION_RESET_FALLBACK=false

# Optional: expected frontend session policy surfaced by
# /activitypods/notifications/status for ops visibility.
EXPECTED_FRONTEND_SESSION_MAX_AGE_MS=28800000
EXPECTED_FRONTEND_POD_REAUTH_DEFER_MS=14400000

# Optional: bounded AT bridge retention. Keep disabled until you are ready to
# enforce finite local history windows for AT projections.
AT_BRIDGE_RETENTION_ENABLED=false
AT_BRIDGE_RETENTION_INTERVAL_MINUTES=60
AT_BRIDGE_RETENTION_BATCH_SIZE=5000
AT_BRIDGE_RETENTION_MAX_BATCHES_PER_RUN=8
AT_BRIDGE_RETENTION_AT_POSTS_DAYS=30
AT_BRIDGE_RETENTION_AT_RECORDS_DAYS=14
AT_BRIDGE_RETENTION_INACTIVE_RECORDS_DAYS=3
AT_BRIDGE_RETENTION_DRY_RUN=false
104 changes: 104 additions & 0 deletions api/AT_BRIDGE_RETENTION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# AT Bridge Retention

Memory's AT bridge stores two large local datasets:

- `at_posts`: feed-friendly projected post rows used by the unified feed and thread hydration.
- `at_records`: raw supported lexicon records used for record inspection and some live derivations such as followed-author resolution.

## Retention policy

The cleanup policy is intentionally collection-aware.

- Keep active follow records indefinitely:
- `app.bsky.graph.follow`
- `canonical.follow`
- Delete inactive `at_records` after a shorter window.
- Delete active non-follow `at_records` after a bounded window.
- Delete old `at_posts` after a bounded window.

Recommended starting values:

- `AT_BRIDGE_RETENTION_INACTIVE_RECORDS_DAYS=3`
- `AT_BRIDGE_RETENTION_AT_RECORDS_DAYS=14`
- `AT_BRIDGE_RETENTION_AT_POSTS_DAYS=30`
- `AT_BRIDGE_RETENTION_BATCH_SIZE=5000`
- `AT_BRIDGE_RETENTION_MAX_BATCHES_PER_RUN=8`
- `AT_BRIDGE_RETENTION_INTERVAL_MINUTES=60`

## Environment variables

- `AT_BRIDGE_RETENTION_ENABLED`: enable the cleanup service. Default `false`.
- `AT_BRIDGE_RETENTION_INTERVAL_MINUTES`: run interval in minutes. Default `60`.
- `AT_BRIDGE_RETENTION_BATCH_SIZE`: max rows deleted per batch. Default `5000`.
- `AT_BRIDGE_RETENTION_MAX_BATCHES_PER_RUN`: max batches per table per run. Default `8`.
- `AT_BRIDGE_RETENTION_AT_POSTS_DAYS`: retention window for `at_posts`. Default `30`.
- `AT_BRIDGE_RETENTION_AT_RECORDS_DAYS`: retention window for active non-follow `at_records`. Default `14`.
- `AT_BRIDGE_RETENTION_INACTIVE_RECORDS_DAYS`: retention window for inactive `at_records`. Default `3`.
- `AT_BRIDGE_RETENTION_DRY_RUN`: log what would be deleted without deleting. Default `false`.

## Safe recovery SQL

For an already bloated local database, reclaim space in this order.

1. Remove inactive raw records in batches:

```sql
WITH doomed AS (
SELECT ctid
FROM at_records
WHERE is_active = false
AND ingested_at < now() - interval '3 days'
LIMIT 5000
)
DELETE FROM at_records
WHERE ctid IN (SELECT ctid FROM doomed);
```

2. Remove old active non-follow raw records in batches:

```sql
WITH doomed AS (
SELECT ctid
FROM at_records
WHERE is_active = true
AND ingested_at < now() - interval '14 days'
AND collection NOT IN ('app.bsky.graph.follow', 'canonical.follow')
LIMIT 5000
)
DELETE FROM at_records
WHERE ctid IN (SELECT ctid FROM doomed);
```

3. Remove old projected AT posts in batches:

```sql
WITH doomed AS (
SELECT ctid
FROM at_posts
WHERE ingested_at < now() - interval '30 days'
LIMIT 5000
)
DELETE FROM at_posts
WHERE ctid IN (SELECT ctid FROM doomed);
```

4. Refresh planner stats after batched deletes:

```sql
ANALYZE at_records;
ANALYZE at_posts;
```

5. During a maintenance window, reclaim table file space:

```sql
VACUUM (VERBOSE, ANALYZE) at_records;
VACUUM (VERBOSE, ANALYZE) at_posts;
```

6. Only after the database has headroom again, rerun thread graph migrations.

## Notes

- Do not blindly delete old follow records. Memory currently derives followed-author thread bumping from active follow records.
- `VACUUM FULL` is intentionally not the default recommendation because it requires additional free space and stronger locking.
56 changes: 56 additions & 0 deletions api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,59 @@ bun run index.ts
```

This project was created using `bun init` in bun v1.1.45. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.

## ActivityPods Notification Interop

The API exposes ActivityPods interoperability resources and notification webhook endpoints used by Memory app registration and inbox notification delivery.

### Relevant environment variables

- `API_URL`: public URL of the Memory API (used in app metadata and webhook callback URLs).
- `FRONTEND_URL`: public URL of the Memory frontend (used for authorization callback endpoint metadata).
- `JWT_SECRET`: used to sign local Memory JWTs and, by default, webhook callback signatures.
- `ACTIVITYPODS_WEBHOOK_SECRET` (optional, recommended): dedicated secret for webhook callback signatures.
- `ACTIVITYPODS_ENABLE_REGISTRATION_RESET_FALLBACK` (optional, default `false`): guarded self-healing fallback for persistent app-registration staleness.
- `EXPECTED_FRONTEND_SESSION_MAX_AGE_MS` (optional, default `28800000`): expected frontend trusted-session max age surfaced by notification status.
- `EXPECTED_FRONTEND_POD_REAUTH_DEFER_MS` (optional, default `14400000`): expected frontend reauth-defer window surfaced by notification status.
- `AT_BRIDGE_RETENTION_ENABLED` and related `AT_BRIDGE_RETENTION_*` variables: bounded cleanup for local AT bridge history. See `AT_BRIDGE_RETENTION.md`.

### Default-safe behavior

By default (`ACTIVITYPODS_ENABLE_REGISTRATION_RESET_FALLBACK=false`), Memory will:

- reconcile registration with `register-app` / `upgrade-app` when needed,
- clean stale or duplicate webhook channels,
- never remove an existing registration automatically.

### Optional registration reset fallback

When `ACTIVITYPODS_ENABLE_REGISTRATION_RESET_FALLBACK=true`, Memory may remove and re-register app grants, but only if all of these conditions are met:

1. provider status remains `upgradeNeeded` after normal reconciliation,
2. no valid inbox webhook is active,
3. fallback is explicitly enabled.

This guardrail minimizes user-impacting changes while still allowing recovery from stuck provider state.

### Production rollout recommendation

1. Start with fallback disabled.
2. Observe bootstrap responses (`registrationReconciled`, `cleanedChannels`, `usedRegistrationResetFallback`) in logs/telemetry.
3. Enable fallback only for environments where `upgradeNeeded` persists and blocks webhook activation.
4. Keep `ACTIVITYPODS_WEBHOOK_SECRET` distinct from `JWT_SECRET` in production.

### Frontend session policy defaults

Recommended baseline defaults for Memory frontend:

- `VITE_SESSION_MAX_AGE_MS=28800000` (8 hours): maximum trusted browser session age before requiring sign-in.
- `VITE_POD_REAUTH_DEFER_MS=14400000` (4 hours): maximum window to defer pod re-authorization prompts while webhook delivery remains healthy.

These values keep re-authentication bounded for security while reducing unnecessary prompt churn during temporary provider staleness.

The notifications status response also includes:

- `expectedFrontendPolicy.sessionMaxAgeMs`
- `expectedFrontendPolicy.podReauthDeferMs`

This allows operators to compare API-side expected policy with frontend runtime logs (`[SessionPolicy] Effective frontend policy`) during rollout validation.
2 changes: 2 additions & 0 deletions api/bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion api/drizzle.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { defineConfig } from 'drizzle-kit'

export default defineConfig({
out: './drizzle',
schema: ['./src/db/schema.ts', './src/db/atBridgeSchema.ts'],
schema: ['./src/db/schema.ts', './src/db/atBridgeSchema.ts', './src/db/chatSchema.ts'],
dialect: 'postgresql',
dbCredentials: {
url: process.env.DB_URL || 'postgres://postgres:postgres@pg:5432/postgres'
Expand Down
Loading
Loading