matrx-seo 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. matrx_seo-0.1.0/.gitignore +271 -0
  2. matrx_seo-0.1.0/CLAUDE.md +32 -0
  3. matrx_seo-0.1.0/DEPLOY.md +165 -0
  4. matrx_seo-0.1.0/Dockerfile +20 -0
  5. matrx_seo-0.1.0/INDEPENDENCE.md +6 -0
  6. matrx_seo-0.1.0/PKG-INFO +25 -0
  7. matrx_seo-0.1.0/README.md +7 -0
  8. matrx_seo-0.1.0/deploy.sh +88 -0
  9. matrx_seo-0.1.0/matrx_seo/FEATURE.md +148 -0
  10. matrx_seo-0.1.0/matrx_seo/__init__.py +97 -0
  11. matrx_seo-0.1.0/matrx_seo/adapters.py +82 -0
  12. matrx_seo-0.1.0/matrx_seo/config.py +85 -0
  13. matrx_seo-0.1.0/matrx_seo/contracts.py +501 -0
  14. matrx_seo-0.1.0/matrx_seo/db/__init__.py +94 -0
  15. matrx_seo-0.1.0/matrx_seo/db/db_requirements.py +17 -0
  16. matrx_seo-0.1.0/matrx_seo/db/grants.yaml +28 -0
  17. matrx_seo-0.1.0/matrx_seo/db/models_host.py +85 -0
  18. matrx_seo-0.1.0/matrx_seo/db/models_seo.py +701 -0
  19. matrx_seo-0.1.0/matrx_seo/fakes.py +90 -0
  20. matrx_seo-0.1.0/matrx_seo/identity.py +51 -0
  21. matrx_seo-0.1.0/matrx_seo/migrations/20260721095300_seo_foundation.sql +682 -0
  22. matrx_seo-0.1.0/matrx_seo/migrations/20260721123000_seo_snapshot_and_binding_followup.sql +88 -0
  23. matrx_seo-0.1.0/matrx_seo/migrations/20260721131500_seo_collection_tasks_and_leases.sql +97 -0
  24. matrx_seo-0.1.0/matrx_seo/migrations/20260721134500_seo_collection_operation.sql +17 -0
  25. matrx_seo-0.1.0/matrx_seo/migrations/20260721143000_seo_collection_receipt.sql +7 -0
  26. matrx_seo-0.1.0/matrx_seo/migrations/20260721150000_seo_provider_call_ledger.sql +72 -0
  27. matrx_seo-0.1.0/matrx_seo/migrations/20260721160000_search_performance_dimensions.sql +26 -0
  28. matrx_seo-0.1.0/matrx_seo/migrations/20260721163327_remove_noncanonical_seo_project_schema.sql +107 -0
  29. matrx_seo-0.1.0/matrx_seo/migrations/20260721170000_web_analytics_ga4_metrics.sql +15 -0
  30. matrx_seo-0.1.0/matrx_seo/migrations/20260721170100_web_analytics_history_identity.sql +14 -0
  31. matrx_seo-0.1.0/matrx_seo/migrations/20260721190000_seo_canonical_rebuild.sql +479 -0
  32. matrx_seo-0.1.0/matrx_seo/orm_identity.py +244 -0
  33. matrx_seo-0.1.0/matrx_seo/orm_repository.py +612 -0
  34. matrx_seo-0.1.0/matrx_seo/pagespeed_history.py +197 -0
  35. matrx_seo-0.1.0/matrx_seo/providers/FEATURE.md +216 -0
  36. matrx_seo-0.1.0/matrx_seo/providers/__init__.py +101 -0
  37. matrx_seo-0.1.0/matrx_seo/providers/bing_webmaster.py +1004 -0
  38. matrx_seo-0.1.0/matrx_seo/providers/brave.py +472 -0
  39. matrx_seo-0.1.0/matrx_seo/providers/dataforseo/__init__.py +24 -0
  40. matrx_seo-0.1.0/matrx_seo/providers/dataforseo/adapter.py +277 -0
  41. matrx_seo-0.1.0/matrx_seo/providers/dataforseo/client.py +883 -0
  42. matrx_seo-0.1.0/matrx_seo/providers/dataforseo/contracts.py +115 -0
  43. matrx_seo-0.1.0/matrx_seo/providers/dataforseo/operations.py +353 -0
  44. matrx_seo-0.1.0/matrx_seo/providers/dataforseo/transport.py +203 -0
  45. matrx_seo-0.1.0/matrx_seo/providers/ga4.py +973 -0
  46. matrx_seo-0.1.0/matrx_seo/providers/gsc.py +653 -0
  47. matrx_seo-0.1.0/matrx_seo/providers/pagespeed.py +586 -0
  48. matrx_seo-0.1.0/matrx_seo/providers/serpapi.py +704 -0
  49. matrx_seo-0.1.0/matrx_seo/rank_matching.py +89 -0
  50. matrx_seo-0.1.0/matrx_seo/repository.py +323 -0
  51. matrx_seo-0.1.0/matrx_seo/service.py +153 -0
  52. matrx_seo-0.1.0/matrx_seo/standalone/__init__.py +3 -0
  53. matrx_seo-0.1.0/matrx_seo/standalone/app.py +421 -0
  54. matrx_seo-0.1.0/pyproject.toml +42 -0
  55. matrx_seo-0.1.0/scripts/independence_gate.sh +72 -0
  56. matrx_seo-0.1.0/tests/conftest.py +126 -0
  57. matrx_seo-0.1.0/tests/fixtures/bing_webmaster_performance.json +24 -0
  58. matrx_seo-0.1.0/tests/fixtures/brave_web_rank_pages.json +51 -0
  59. matrx_seo-0.1.0/tests/fixtures/ga4/metadata.json +11 -0
  60. matrx_seo-0.1.0/tests/fixtures/ga4/report_page_1.json +52 -0
  61. matrx_seo-0.1.0/tests/fixtures/ga4/report_page_2.json +52 -0
  62. matrx_seo-0.1.0/tests/fixtures/gsc_search_analytics_pages.json +33 -0
  63. matrx_seo-0.1.0/tests/fixtures/pagespeed/mobile_full.json +138 -0
  64. matrx_seo-0.1.0/tests/fixtures/serpapi/google_rank_austin.json +76 -0
  65. matrx_seo-0.1.0/tests/test_bing_webmaster.py +468 -0
  66. matrx_seo-0.1.0/tests/test_bing_webmaster_live.py +6 -0
  67. matrx_seo-0.1.0/tests/test_brave_live.py +83 -0
  68. matrx_seo-0.1.0/tests/test_brave_rank_adapter.py +385 -0
  69. matrx_seo-0.1.0/tests/test_collection_service.py +502 -0
  70. matrx_seo-0.1.0/tests/test_dataforseo.py +895 -0
  71. matrx_seo-0.1.0/tests/test_dataforseo_live.py +49 -0
  72. matrx_seo-0.1.0/tests/test_ga4_live.py +6 -0
  73. matrx_seo-0.1.0/tests/test_ga4_provider.py +463 -0
  74. matrx_seo-0.1.0/tests/test_gsc_live.py +6 -0
  75. matrx_seo-0.1.0/tests/test_gsc_provider.py +408 -0
  76. matrx_seo-0.1.0/tests/test_independence.py +29 -0
  77. matrx_seo-0.1.0/tests/test_orm_repository_live.py +339 -0
  78. matrx_seo-0.1.0/tests/test_pagespeed_live.py +72 -0
  79. matrx_seo-0.1.0/tests/test_pagespeed_provider.py +460 -0
  80. matrx_seo-0.1.0/tests/test_rank_matching.py +50 -0
  81. matrx_seo-0.1.0/tests/test_serpapi_live.py +97 -0
  82. matrx_seo-0.1.0/tests/test_serpapi_provider.py +348 -0
  83. matrx_seo-0.1.0/tests/test_standalone_boot.py +89 -0
@@ -0,0 +1,271 @@
1
+ *.pyc
2
+ secrets/
3
+ ignore/
4
+ temp/
5
+ logs/
6
+ # The broad `logs/` rule above is for RUNTIME log output, but it also matched
7
+ # the dashboard's SOURCE directory and silently swallowed an entire feature's
8
+ # files (only the pre-existing index.tsx stayed tracked), breaking the prod
9
+ # Docker build with "Could not resolve ./structured-tab". Re-include the source.
10
+ !apps/dashboard/src/features/logs/
11
+ !apps/dashboard/src/features/logs/**
12
+ todo
13
+ text_notes/
14
+ aidream/secrets/2.env
15
+ automation_matrix/matrix_processing/temp/*
16
+ cd
17
+ # Byte-compiled / optimized / DLL files
18
+ __pycache__/
19
+ *.py[cod]
20
+ *$py.class
21
+
22
+ # C extensions
23
+ *.so
24
+ .venv/
25
+
26
+ # Distribution / packaging
27
+ .Python
28
+ build/
29
+ develop-eggs/
30
+ dist/
31
+ downloads/
32
+ eggs/
33
+ .eggs/
34
+ lib/
35
+ lib64/
36
+ # The blanket lib/ rule above is from the standard Python .gitignore template
37
+ # and was silently swallowing TS source under the SPA `src/lib/` folders.
38
+ # Re-allow them explicitly so frontend builds don't ship without their lib layer.
39
+ !apps/dashboard/src/lib/
40
+ !apps/dashboard/src/lib/**
41
+ !apps/workflow-studio/src/lib/
42
+ !apps/workflow-studio/src/lib/**
43
+ parts/
44
+ sdist/
45
+ var/
46
+ wheels/
47
+ share/python-wheels/
48
+ *.egg-info/
49
+ .installed.cfg
50
+ *.egg
51
+ MANIFEST
52
+
53
+ # PyInstaller
54
+ # Usually these files are written by a python script from a template
55
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
56
+ *.manifest
57
+ *.spec
58
+
59
+ # Installer logs
60
+ pip-log.txt
61
+ pip-delete-this-directory.txt
62
+
63
+ # Unit test / coverage reports
64
+ ai/tests/clean_response.json
65
+ ai/tests/cx_storage_response.json
66
+ ai/tests/execution_test.py
67
+ ai/tests/final_response.json
68
+ htmlcov/
69
+ .tox/
70
+ .nox/
71
+ .coverage
72
+ .coverage.*
73
+ .cache
74
+ nosetests.xml
75
+ coverage.xml
76
+ *.cover
77
+ *.py,cover
78
+ .hypothesis/
79
+ .pytest_cache/
80
+ cover/
81
+
82
+ # Translations
83
+ *.mo
84
+ *.pot
85
+
86
+ # Django stuff:
87
+ *.log
88
+ local_settings.py
89
+ db.sqlite3
90
+ db.sqlite3-journal
91
+
92
+ # Flask stuff:
93
+ instance/
94
+ .webassets-cache
95
+
96
+ # Scrapy stuff:
97
+ .scrapy
98
+
99
+ # Sphinx documentation
100
+ docs/_build/
101
+
102
+ # PyBuilder
103
+ .pybuilder/
104
+ target/
105
+
106
+ # Jupyter Notebook
107
+ .ipynb_checkpoints
108
+
109
+ # IPython
110
+ profile_default/
111
+ ipython_config.py
112
+
113
+ # pyenv
114
+ # For a library or package, you might want to ignore these files since the code is
115
+ # intended to run in multiple environments; otherwise, check them in:
116
+ # .python-version
117
+
118
+ # pipenv
119
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
120
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
121
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
122
+ # install all needed dependencies.
123
+ #Pipfile.lock
124
+
125
+ # poetry
126
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
127
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
128
+ # commonly ignored for libraries.
129
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
130
+
131
+ # pdm
132
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
133
+ #pdm.lock
134
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
135
+ # in version control.
136
+ # https://pdm.fming.dev/#use-with-ide
137
+ .pdm.toml
138
+
139
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
140
+ __pypackages__/
141
+
142
+ # Celery stuff
143
+ celerybeat-schedule
144
+ celerybeat.pid
145
+
146
+ # SageMath parsed files
147
+ *.sage.py
148
+
149
+ # Environments
150
+ .env
151
+ .env_remote
152
+ .venv
153
+ env/
154
+ venv/
155
+ ENV/
156
+ env.bak/
157
+ venv.bak/
158
+ .env.armanonly
159
+
160
+ # Spyder project settings
161
+ .spyderproject
162
+ .spyproject
163
+
164
+ # Rope project settings
165
+ .ropeproject
166
+
167
+ # mkdocs documentation
168
+ /site
169
+
170
+ # mypy
171
+ .mypy_cache/
172
+ .dmypy.json
173
+ dmypy.json
174
+
175
+ # Pyre type checker
176
+ .pyre/
177
+
178
+ # random armani files
179
+ /armani_dev/secrets/
180
+ /armani/
181
+ /_armani/
182
+
183
+
184
+
185
+ # pytype static type analyzer
186
+ .pytype/
187
+
188
+ # Cython debug symbols
189
+ cython_debug/
190
+
191
+ .idea/
192
+ .vscode/
193
+ /node_modules/
194
+
195
+ # Frontend pnpm workspace (apps/) — node_modules at the workspace root and any
196
+ # member, plus Vite caches and build output. The unified lockfile (apps/pnpm-lock.yaml)
197
+ # IS committed; everything below is regenerated.
198
+ node_modules/
199
+ apps/**/.vite/
200
+ apps/**/dist/
201
+ .vite/
202
+
203
+ dump.rdb
204
+
205
+ frontend/
206
+
207
+ # AME Temp Files and directory structure
208
+ # Ignore all files in the temp directory and its subdirectories
209
+ /temp/**/*
210
+ /tmp/**/*
211
+
212
+ # Allow .gitkeep files to retain directory structure
213
+ !/temp/**/.gitkeep
214
+ !/tmp/**/.gitkeep
215
+
216
+ # Armani
217
+ .history*
218
+ .history/
219
+ local_data/
220
+ local_reports_data/
221
+ webscraper/quick_scrapes/temp/
222
+ automation_matrix/ai_apis/fireworks/_dev/*
223
+ automation_matrix/ai_apis/fireworks/_dev/fireworks_sample.py
224
+ *.pdf
225
+ *.flac
226
+ *.mp3
227
+ *.wav
228
+ miniconda.sh
229
+ /database/python_sql/temp_data/
230
+ .history*
231
+ .history/
232
+ .history/
233
+
234
+ _dev/
235
+ /_dev/
236
+ requirements_filtered.txt
237
+
238
+ # matrx-dev-tools backups
239
+ .env-backups/
240
+ # Matrx Ship config (contains API key)
241
+ .matrx-ship.json
242
+
243
+ # Matrx config (contains API keys)
244
+ .matrx.json
245
+ .matrx-tools.conf
246
+
247
+ # Claude Code local worktrees and per-user settings
248
+ .claude/worktrees/
249
+ .claude/settings.local.json
250
+
251
+ # Append-only snapshots from matrx_utils.update_history (unbounded; do not commit)
252
+ common/utils/data_in_code/data_history.json
253
+ packages/matrx-utils/matrx_utils/data_in_code/data_history.json
254
+
255
+ # Tool-dispatch debug logs — one file per server start, never committed
256
+ .matrx-debug/
257
+
258
+ # macOS Finder metadata
259
+ .DS_Store
260
+ **/.DS_Store
261
+
262
+ # Environment files
263
+ .env
264
+ .env.*
265
+ *.env
266
+ *.env.*
267
+
268
+ # Keep safe templates trackable
269
+ !.env.example
270
+ !.env.sample
271
+ !.env.template
@@ -0,0 +1,32 @@
1
+ # CLAUDE.md — matrx-seo
2
+
3
+ `matrx-seo` is the SEO vertical: provider/client layer + canonical ORM persistence over
4
+ the live `seo.*` schema. Read `matrx_seo/FEATURE.md` before changing this package — it
5
+ is the durable contract (repository semantics, identity names, association-only project
6
+ linkage, battery credentials).
7
+
8
+ Hard rules:
9
+
10
+ - Persistence uses canonical `web.site` / `web.page` ids and links keywords to
11
+ `workspace.projects` ONLY through `platform.associations`
12
+ (`orm_identity.link_keyword_to_project`). No table may contain `project_id`; no
13
+ feature-specific project membership or site/page binding table may exist.
14
+ - All DB access goes through matrx-orm models (`db/models_seo.py`, host mirrors in
15
+ `db/models_host.py`) — zero raw SQL.
16
+ - Credentials resolve through the matrx-orm secrets battery
17
+ (`config.battery_credential_resolver`); no environment credential fallback.
18
+ - Never import `aidream` or a repository-root module from `matrx_seo/` source
19
+ (tests may; only package source is boundary-checked).
20
+
21
+ The standalone service is REAL (`standalone/app.py::create_app`): matrx-connect
22
+ `AuthMiddleware` (Supabase JWTs only), boot gates (svc_seo pool from
23
+ `SUPABASE_MATRIX_*`, `assert_service_role()`, explicit
24
+ `CREDENTIALS_ENCRYPTION_KEY` round-trip, JWT material from
25
+ `SUPABASE_MATRIX_URL`/`SUPABASE_JWT_SECRET`), and thin JSON routes over ONE
26
+ `SeoCollectionService` (`/health`, `/health/ready`, `POST/GET /collections`,
27
+ `GET /collections/{run_id}`). Standalone provider registry =
28
+ `ADAPTER_FACTORIES` (brave / dataforseo / serpapi — the others need host client
29
+ factories). MCP tools were deliberately skipped (auth-stack cost — see
30
+ FEATURE.md). `scripts/independence_gate.sh` pins the boot contract in a bare
31
+ venv; `tests/test_standalone_boot.py` pins it in the suite. matrx-connect is a
32
+ `[standalone]`-extra dependency, not a core one.
@@ -0,0 +1,165 @@
1
+ # Deploying matrx-seo (the SEO vertical service)
2
+
3
+ STATUS 2026-07-21: **artifacts ready, NOT yet deployed.** Everything below is
4
+ prepared; the first deploy needs three human actions (marked **[ARMAN]**) that
5
+ an agent cannot do: an AWS SSO login, a one-time PyPI Trusted Publisher, and
6
+ minting the `svc_seo` database password.
7
+
8
+ The service runs **on the same EC2 box as matrx-files** (`i-084f757c1e47d4efb`
9
+ / `54.144.86.132`, Ship name `matrx-sandbox-host-dev`) — same pattern, same
10
+ management surface, different container/port/hostname.
11
+
12
+ | | matrx-files | matrx-seo |
13
+ |---|---|---|
14
+ | container | `matrx-files` | `matrx-seo` |
15
+ | port (loopback only) | `127.0.0.1:8080` | `127.0.0.1:8081` |
16
+ | env file (root 600) | `/etc/matrx-files.env` | `/etc/matrx-seo.env` |
17
+ | build dir on box | `/opt/matrx-files` | `/opt/matrx-seo` |
18
+ | public URL | `https://files.matrxserver.com` | `https://seo.matrxserver.com` |
19
+ | TLS | shared Caddy container (`matrx-files-tls`, host network, :443) | **same container, new site block** |
20
+
21
+ **One Caddy for the box.** Do NOT start a second TLS container — :443 is taken.
22
+ Add a `seo.matrxserver.com` block to the existing Caddyfile and reload.
23
+
24
+ ---
25
+
26
+ ## The database contract — there is ONE database
27
+
28
+ The service connects to **Matrx Main**, the same Postgres everything else uses,
29
+ through the **same env var names** (`SUPABASE_MATRIX_*`). There is no SEO
30
+ database and no SEO-specific connection variable.
31
+
32
+ What differs on THIS deployment is only the **credentials**: `SUPABASE_MATRIX_USER`
33
+ is `svc_seo`, a role that owns the `seo` schema and is granted exactly the
34
+ platform surfaces in [`matrx_seo/db/grants.yaml`](matrx_seo/db/grants.yaml).
35
+ The service asserts this at boot (`assert_service_role`) and **crashes if handed
36
+ a broad role** — that assertion is what makes the boundary real rather than
37
+ documentation.
38
+
39
+ ### `/etc/matrx-seo.env` (root 600)
40
+
41
+ ```ini
42
+ # The ONE database (identical names to aidream/scraper/files).
43
+ SUPABASE_MATRIX_HOST=<same as every other service>
44
+ SUPABASE_MATRIX_PORT=6543
45
+ SUPABASE_MATRIX_DATABASE_NAME=postgres
46
+ SUPABASE_MATRIX_USER=svc_seo # <- the ONLY per-deployment difference
47
+ SUPABASE_MATRIX_PASSWORD=<minted below> # <- svc_seo's password
48
+
49
+ # Supabase JWT verification (JWKS) — same value every service uses.
50
+ SUPABASE_MATRIX_URL=<same as every other service>
51
+
52
+ # The ONE platform encryption key (matrx-orm secrets battery). Same value as
53
+ # aidream/scraper — provider API keys are read straight from users.user_secrets.
54
+ CREDENTIALS_ENCRYPTION_KEY=<same as every other service>
55
+ ```
56
+
57
+ Every one of these already exists on the other servers. **Copy the values; do
58
+ not invent new ones.**
59
+
60
+ ---
61
+
62
+ ## First deploy — the runbook
63
+
64
+ ### 0. **[ARMAN]** One-time PyPI Trusted Publisher
65
+ `https://pypi.org/manage/account/publishing/` → pending publisher:
66
+ owner `AI-Matrix-Engine`, repo `aidream`, workflow `publish-package.yml`,
67
+ environment blank, project `matrx-seo`. (Same one-time step every workspace
68
+ package needed.)
69
+
70
+ ### 1. **[ARMAN]** Mint the `svc_seo` login
71
+ The role exists and owns its schema, but is `NOLOGIN` with no password — it
72
+ cannot connect until you give it one. In the Supabase SQL editor:
73
+
74
+ ```sql
75
+ ALTER ROLE svc_seo LOGIN PASSWORD '<a strong password you generate>';
76
+ ```
77
+
78
+ Put that same password in `/etc/matrx-seo.env` as `SUPABASE_MATRIX_PASSWORD`.
79
+ Nobody needs to see it again — the drift guard never reads it, and the service
80
+ only proves *which role* it connected as.
81
+
82
+ > Keeping it `NOLOGIN` until deploy day is deliberate: the boundary exists in
83
+ > the database from the moment the schema did, but no credential existed to
84
+ > leak in the meantime.
85
+
86
+ ### 2. **[ARMAN]** AWS SSO (the deploy script rides SSM)
87
+ ```bash
88
+ aws sso login --profile AdministratorAccess-872515272894
89
+ ssh matrx-sandbox 'echo ok' # must print ok
90
+ ```
91
+
92
+ ### 3. Publish the package
93
+ ```bash
94
+ cd ~/code/aidream
95
+ git tag matrx-seo/v0.1.0 && git push origin refs/tags/matrx-seo/v0.1.0
96
+ # CI publishes to PyPI; confirm: https://pypi.org/project/matrx-seo/
97
+ ```
98
+ matrx-orm must also be published at a version containing the secrets battery +
99
+ role-aware associations (tag `matrx-orm/v<version>` the same way) — the
100
+ Dockerfile resolves it from PyPI.
101
+
102
+ ### 4. Put the env file on the box
103
+ ```bash
104
+ scp /tmp/matrx-seo.env matrx-sandbox:/tmp/matrx-seo.env
105
+ ssh matrx-sandbox 'sudo mv /tmp/matrx-seo.env /etc/matrx-seo.env && sudo chown root:root /etc/matrx-seo.env && sudo chmod 600 /etc/matrx-seo.env'
106
+ ```
107
+
108
+ ### 5. Deploy
109
+ ```bash
110
+ cd ~/code/aidream/packages/matrx-seo
111
+ ./deploy.sh 0.1.0
112
+ ```
113
+ The script refuses to swap the live container unless the built image really
114
+ contains that version, and after the swap it requires readiness to report
115
+ `service_role: true` — i.e. it proves the service came up **inside its granted
116
+ boundary**.
117
+
118
+ ### 6. TLS + DNS (first deploy only)
119
+ Add the site block to the box's shared Caddyfile (`/opt/matrx-files/Caddyfile`):
120
+
121
+ ```
122
+ seo.matrxserver.com {
123
+ tls internal
124
+ reverse_proxy 127.0.0.1:8081
125
+ }
126
+ ```
127
+ Then `ssh matrx-sandbox 'sudo docker restart matrx-files-tls'`.
128
+
129
+ Cloudflare: A record `seo.matrxserver.com` → `54.144.86.132`, **proxied**,
130
+ SSL mode Full (already set for the zone). Security group `sg-05a1b5a6163cd8ee6`
131
+ already allows 443 from Cloudflare ranges only — **no new SG rule**, and 8081
132
+ stays closed to the world (the app binds loopback).
133
+
134
+ ### 7. Register in Ship
135
+ Already recorded in [`matrx-ship/SERVERS_AND_ROUTES.md`](/Users/armanisadeghi/code/matrx-ship/SERVERS_AND_ROUTES.md).
136
+ Drop the service descriptor on the box so the Manager UI lists it:
137
+ ```bash
138
+ ssh matrx-sandbox 'sudo tee /opt/matrx-seo/service.json >/dev/null <<JSON
139
+ {"name":"matrx-seo","container":"matrx-seo","port":8081,
140
+ "public_url":"https://seo.matrxserver.com","health":"/health",
141
+ "readiness":"/health/ready","env_file":"/etc/matrx-seo.env",
142
+ "repo":"aidream/packages/matrx-seo"}
143
+ JSON'
144
+ ```
145
+
146
+ ---
147
+
148
+ ## Manage
149
+
150
+ `ssh matrx-sandbox`, or the Manager host-exec
151
+ (`POST /api/hosts/matrx-sandbox-host-dev/exec` → `sudo docker …`).
152
+
153
+ - App: `sudo docker {ps,logs,restart} matrx-seo`
154
+ - TLS: `sudo docker {logs,restart} matrx-files-tls` (shared)
155
+ - **Env changes need `docker rm` + `run`, not `restart`** — `--env-file` is read
156
+ only at run time. Re-running `./deploy.sh <version>` does this correctly.
157
+ - Rollback: `./deploy.sh <previous-version>`
158
+
159
+ ## Health contract
160
+
161
+ - `GET /health` — liveness, unauthenticated.
162
+ - `GET /health/ready` — 503 until every boot gate passes; the body names the
163
+ failed component (`database` / `service_role` / `models` /
164
+ `credentials_encryption` / `service`).
165
+ - Every `/collections*` route requires a Supabase JWT (401 otherwise).
@@ -0,0 +1,20 @@
1
+ # Matrx SEO — standalone vertical service image.
2
+ #
3
+ # Installs ENTIRELY from PyPI (every matrx-* dep is published; a bare
4
+ # `uv pip install matrx-seo[standalone]` is proven by the independence gate).
5
+ # Build from anywhere:
6
+ # docker build -t matrx-seo packages/matrx-seo
7
+ # Pin the version at build time for reproducible deploys:
8
+ # docker build --build-arg MATRX_SEO_VERSION==0.1.0 -t matrx-seo:0.1.0 packages/matrx-seo
9
+ #
10
+ # No system binaries: this service talks HTTP to SEO providers and Postgres to
11
+ # the ONE database. Nothing to render, nothing to OCR — keep the image small.
12
+ FROM python:3.13-slim
13
+
14
+ RUN pip install --no-cache-dir uv
15
+
16
+ ARG MATRX_SEO_VERSION=""
17
+ RUN uv pip install --system "matrx-seo[standalone]${MATRX_SEO_VERSION}"
18
+
19
+ EXPOSE 8081
20
+ CMD ["uvicorn", "matrx_seo.standalone.app:create_app", "--factory", "--host", "0.0.0.0", "--port", "8081"]
@@ -0,0 +1,6 @@
1
+ # Package independence
2
+
3
+ This package follows `/Users/armanisadeghi/code/matrx-package-template/INDEPENDENCE.md`:
4
+ no host imports, explicit injected configuration, and package-owned contracts,
5
+ schema manifest, models, migrations, tests, and standalone boot surface.
6
+
@@ -0,0 +1,25 @@
1
+ Metadata-Version: 2.4
2
+ Name: matrx-seo
3
+ Version: 0.1.0
4
+ Summary: Provider-neutral SEO collection contracts and canonical persistence.
5
+ Requires-Python: >=3.13
6
+ Requires-Dist: httpx>=0.28
7
+ Requires-Dist: matrx-orm>=3.1
8
+ Requires-Dist: matrx-utils
9
+ Requires-Dist: pydantic>=2.12
10
+ Provides-Extra: dev
11
+ Requires-Dist: pytest-asyncio>=0.25; extra == 'dev'
12
+ Requires-Dist: pytest>=8.3; extra == 'dev'
13
+ Provides-Extra: standalone
14
+ Requires-Dist: fastapi>=0.115; extra == 'standalone'
15
+ Requires-Dist: matrx-connect; extra == 'standalone'
16
+ Requires-Dist: uvicorn>=0.34; extra == 'standalone'
17
+ Description-Content-Type: text/markdown
18
+
19
+ # matrx-seo
20
+
21
+ Provider-neutral SEO provider clients, collection contracts, normalization, replay
22
+ fixtures, and canonical persistence for Matrx. Durable persistence is temporarily absent
23
+ while it is rebuilt on `platform.associations`; persistence remains part of this package's
24
+ required product boundary. See
25
+ [`matrx_seo/FEATURE.md`](matrx_seo/FEATURE.md) for the current contract.
@@ -0,0 +1,7 @@
1
+ # matrx-seo
2
+
3
+ Provider-neutral SEO provider clients, collection contracts, normalization, replay
4
+ fixtures, and canonical persistence for Matrx. Durable persistence is temporarily absent
5
+ while it is rebuilt on `platform.associations`; persistence remains part of this package's
6
+ required product boundary. See
7
+ [`matrx_seo/FEATURE.md`](matrx_seo/FEATURE.md) for the current contract.
@@ -0,0 +1,88 @@
1
+ #!/usr/bin/env bash
2
+ # Deploy matrx-seo to the live EC2 box behind https://seo.matrxserver.com
3
+ #
4
+ # ./deploy.sh 0.1.0 # deploy a version (must already be on PyPI)
5
+ # ./deploy.sh 0.0.9 # rolling back is the same command
6
+ #
7
+ # Mirrors packages/matrx-files/deploy.sh (same box, same pattern) and REFUSES
8
+ # to touch the live container until the new image is proven:
9
+ # 1. checks PyPI actually has the version (tag push publishes it)
10
+ # 2. syncs THIS directory's canonical Dockerfile to the box (kills drift)
11
+ # 3. builds matrx-seo:<version> on the box
12
+ # 4. asserts the built image really contains <version> (aborts otherwise)
13
+ # 5. swaps the container (rm + run — NEVER docker restart: --env-file is
14
+ # read only at run time)
15
+ # 6. health-checks locally and through Cloudflare, and proves the service
16
+ # is inside its granted DB boundary (readiness reports service_role)
17
+ #
18
+ # Requires: `ssh matrx-sandbox` working (SSM per ~/.ssh/config; run
19
+ # `aws sso login --profile AdministratorAccess-872515272894` first if stale).
20
+ #
21
+ # The box needs /etc/matrx-seo.env (root 600) BEFORE the first deploy — see
22
+ # DEPLOY.md. It carries the ONE database's standard SUPABASE_MATRIX_* vars
23
+ # with USER/PASSWORD set to the svc_seo role, plus CREDENTIALS_ENCRYPTION_KEY.
24
+ set -euo pipefail
25
+
26
+ VERSION="${1:?usage: deploy.sh <version, e.g. 0.1.0>}"
27
+ HOST="${MATRX_SEO_HOST:-matrx-sandbox}"
28
+ PUBLIC_BASE="https://seo.matrxserver.com"
29
+ PORT=8081
30
+ DIR="$(cd "$(dirname "$0")" && pwd)"
31
+
32
+ echo "==> [1/6] checking PyPI has matrx-seo==${VERSION}"
33
+ if ! curl -sf -m 15 "https://pypi.org/pypi/matrx-seo/${VERSION}/json" >/dev/null; then
34
+ echo "ERROR: matrx-seo==${VERSION} is not on PyPI."
35
+ echo " Publish first: cd aidream && git tag matrx-seo/v${VERSION} && git push origin refs/tags/matrx-seo/v${VERSION}"
36
+ exit 1
37
+ fi
38
+
39
+ echo "==> [2/6] syncing canonical Dockerfile to ${HOST}:/opt/matrx-seo/"
40
+ ssh "${HOST}" 'sudo mkdir -p /opt/matrx-seo'
41
+ scp -q "${DIR}/Dockerfile" "${HOST}:/tmp/matrx-seo.Dockerfile"
42
+ ssh "${HOST}" 'sudo mv /tmp/matrx-seo.Dockerfile /opt/matrx-seo/Dockerfile'
43
+
44
+ echo "==> [3/6] building matrx-seo:${VERSION} on ${HOST}"
45
+ ssh "${HOST}" "sudo docker build --build-arg MATRX_SEO_VERSION='==${VERSION}' -t 'matrx-seo:${VERSION}' /opt/matrx-seo"
46
+
47
+ echo "==> [4/6] verifying the image really contains ${VERSION}"
48
+ GOT="$(ssh "${HOST}" "sudo docker run --rm --entrypoint python 'matrx-seo:${VERSION}' -c 'import importlib.metadata as m; print(m.version(\"matrx-seo\"))'" | tr -d '[:space:]')"
49
+ if [ "${GOT}" != "${VERSION}" ]; then
50
+ echo "ERROR: built image contains matrx-seo ${GOT}, expected ${VERSION} — ABORTING; live container untouched."
51
+ exit 1
52
+ fi
53
+ echo " image verified: ${GOT}"
54
+
55
+ echo "==> [5/6] swapping the live container (rm + run)"
56
+ ssh "${HOST}" "sudo docker rm -f matrx-seo >/dev/null 2>&1 || true; sudo docker run -d --name matrx-seo --restart unless-stopped --env-file /etc/matrx-seo.env -p 127.0.0.1:${PORT}:${PORT} 'matrx-seo:${VERSION}'"
57
+
58
+ echo "==> [6/6] health checks"
59
+ HEALTH=""
60
+ for _ in $(seq 1 20); do
61
+ HEALTH="$(ssh "${HOST}" "curl -sf -m 5 http://127.0.0.1:${PORT}/health" 2>/dev/null || true)"
62
+ [ -n "${HEALTH}" ] && break
63
+ sleep 2
64
+ done
65
+ if [ -z "${HEALTH}" ]; then
66
+ echo "ERROR: health never came up on the box. Logs: ssh ${HOST} 'sudo docker logs --tail 50 matrx-seo'"
67
+ echo " Roll back: ./deploy.sh <previous-version>"
68
+ exit 1
69
+ fi
70
+ echo " box health: ${HEALTH}"
71
+
72
+ # Readiness proves the BOUNDARY: the service booted only because it connected
73
+ # as svc_seo and the encryption key round-tripped. A 503 here means a boot gate
74
+ # refused — read the body, it names the failed component.
75
+ READY="$(ssh "${HOST}" "curl -s -m 10 http://127.0.0.1:${PORT}/health/ready" 2>/dev/null || true)"
76
+ echo " box readiness: ${READY}"
77
+ case "${READY}" in
78
+ *'"service_role": true'*|*'"service_role":true'*) echo " boundary OK: connected as svc_seo" ;;
79
+ *) echo "ERROR: readiness does not confirm the svc_seo boundary — ABORTING the rollout check."
80
+ echo " Logs: ssh ${HOST} 'sudo docker logs --tail 50 matrx-seo'"
81
+ exit 1 ;;
82
+ esac
83
+
84
+ echo " public health: $(curl -sf -m 10 "${PUBLIC_BASE}/health" || echo 'UNREACHABLE (DNS/Caddy not wired yet?)')"
85
+ ANON="$(curl -s -m 10 -o /dev/null -w '%{http_code}' "${PUBLIC_BASE}/collections?organization_id=00000000-0000-0000-0000-000000000000")"
86
+ echo " anon /collections -> HTTP ${ANON} (expect 401)"
87
+
88
+ echo "DONE: matrx-seo ${VERSION} is live at ${PUBLIC_BASE}. Rollback = ./deploy.sh <previous-version>"