Skip to content

Commit 636c0bc

Browse files
etewiahclaude
andcommitted
Update SPP integration docs to reflect completed implementation
Mark all docs as Implemented, fix route paths (spp_publish not publish), correct authentication code samples, add content management endpoint spec, update cache TTL references, and mark checklists as done. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent b38e87f commit 636c0bc

File tree

6 files changed

+179
-101
lines changed

6 files changed

+179
-101
lines changed

docs/api/spp/README.md

Lines changed: 31 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# SPP–PWB Integration
22

3+
**Status:** Implemented (Phases 1–6 complete)
4+
35
SinglePropertyPages (SPP) is an Astro.js application that generates standalone marketing microsites for individual property listings. It integrates with PropertyWebBuilder (PWB) as a data backend.
46

57
**Deployment model:** Option B — SPP serves pages independently on its own domain. PWB is a data API. See [Architecture](#architecture) below.
@@ -10,14 +12,14 @@ SinglePropertyPages (SPP) is an Astro.js application that generates standalone m
1012

1113
## Documents
1214

13-
| Document | What It Covers |
14-
|----------|---------------|
15-
| [SppListing Model](./spp-listing-model.md) | Data model, migration, model definition, relationship to existing listings |
16-
| [Endpoints](./endpoints.md) | Publish, unpublish, leads API specs + enquiry linking + response conventions |
17-
| [Authentication](./authentication.md) | API key auth via `X-API-Key` header and `WebsiteIntegration` |
18-
| [CORS](./cors.md) | `rack-cors` configuration for SPP origins |
19-
| [SEO](./seo.md) | Canonical URLs, sitemaps, JSON-LD coordination between PWB and SPP |
20-
| [Data Freshness](./data-freshness.md) | Cache headers (phase 1) and webhooks (phase 2) |
15+
| Document | What It Covers | Status |
16+
|----------|---------------|--------|
17+
| [SppListing Model](./spp-listing-model.md) | Data model, migration, model definition, relationship to existing listings | Implemented |
18+
| [Endpoints](./endpoints.md) | Publish, unpublish, leads, content management API specs + enquiry linking | Implemented |
19+
| [Authentication](./authentication.md) | API key auth via `X-API-Key` header and `WebsiteIntegration` | Implemented |
20+
| [CORS](./cors.md) | `rack-cors` configuration for SPP origins | Implemented |
21+
| [SEO](./seo.md) | Canonical URLs, sitemaps, JSON-LD coordination between PWB and SPP | Implemented |
22+
| [Data Freshness](./data-freshness.md) | Cache headers (phase 1) and webhooks (phase 2, future) | Phase 1 done |
2123

2224
---
2325

@@ -113,16 +115,29 @@ CORS is required for this cross-origin POST. See [CORS](./cors.md).
113115

114116
---
115117

116-
## Key Reference Files
118+
## Provisioning
119+
120+
Use the rake task to create an SPP integration for a tenant:
121+
122+
```bash
123+
rails spp:provision[my-subdomain]
124+
```
125+
126+
This creates a `WebsiteIntegration` (category: `spp`) with an encrypted API key and outputs the environment variables SPP needs.
127+
128+
## Key Implementation Files
117129

118130
| File | Relevance |
119131
|------|-----------|
120-
| `app/models/pwb/sale_listing.rb` | Listing model pattern that SppListing mirrors |
121-
| `app/models/concerns/listing_stateable.rb` | Shared state management |
122-
| `app/controllers/api_manage/v1/base_controller.rb` | API key authentication |
123-
| `app/controllers/api_public/v1/enquiries_controller.rb` | Enquiry endpoint |
132+
| `app/models/pwb/spp_listing.rb` | SppListing model with Mobility translations, monetize, curated photos |
133+
| `app/controllers/api_manage/v1/spp_listings_controller.rb` | Publish, unpublish, leads, and content management endpoints |
134+
| `app/controllers/api_manage/v1/base_controller.rb` | API key authentication (iterates encrypted credentials) |
135+
| `app/controllers/api_public/v1/enquiries_controller.rb` | Enquiry endpoint (links messages to properties) |
136+
| `app/controllers/api_public/v1/base_controller.rb` | Cache headers (`expires_in 1.hour`) |
124137
| `app/controllers/concerns/subdomain_tenant.rb` | Tenant resolution via `X-Website-Slug` |
125-
| `config/initializers/cors.rb` | CORS configuration |
126-
| `app/helpers/seo_helper.rb` | SEO meta tags, JSON-LD, canonical URLs |
127-
| `app/controllers/sitemaps_controller.rb` | Sitemap generation |
138+
| `app/helpers/seo_helper.rb` | `spp_live_url_for` helper, SEO meta tags, JSON-LD, canonical URLs |
139+
| `app/controllers/sitemaps_controller.rb` | Sitemap generation (uses SPP URLs when available) |
140+
| `app/views/sitemaps/index.xml.erb` | Sitemap template with SPP URL support |
141+
| `config/initializers/cors.rb` | CORS configuration with SPP origin patterns |
142+
| `lib/tasks/spp.rake` | SPP provisioning rake task |
128143
| `docs/architecture/per_tenant_astro_url_routing.md` | Per-tenant URL config pattern |

docs/api/spp/authentication.md

Lines changed: 25 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# SPP Authentication for api_manage Endpoints
22

3-
**Status:** Proposed
3+
**Status:** Implemented
44
**Related:** [SPP–PWB Integration](./README.md) | [CORS](./cors.md)
55

66
---
@@ -43,9 +43,10 @@ Request with X-API-Key header
4343
4444
api_manage BaseController#authenticate_from_api_key
4545
46-
├── Look up: current_website.integrations.find_by(api_key: key, active: true)
46+
├── Iterate: current_website.integrations.enabled.find { |i| i.credential('api_key') == key }
47+
│ (API keys are stored in encrypted credentials, so each must be decrypted and compared)
4748
48-
├── If found: Return website's owner or first admin as acting user
49+
├── If found: Call integration.record_usage!, return website's owner or first admin
4950
5051
└── If not found: Return nil (authentication fails)
5152
```
@@ -62,8 +63,20 @@ In `app/models/pwb/website_integration.rb`, add `:spp` to the categories enum if
6263

6364
### Provisioning the API Key
6465

66+
Use the provisioning rake task:
67+
68+
```bash
69+
rails spp:provision[my-subdomain]
70+
```
71+
72+
This is idempotent — running it again for the same subdomain outputs the existing key. The task:
73+
1. Finds the website by subdomain
74+
2. Creates a `WebsiteIntegration` (category: `spp`, provider: `single_property_pages`) with an encrypted API key
75+
3. Outputs the API key and environment variables for SPP
76+
77+
Alternatively, via Rails console:
78+
6579
```ruby
66-
# Via Rails console or admin interface:
6780
website = Pwb::Website.find_by(subdomain: 'my-tenant')
6881

6982
integration = website.integrations.create!(
@@ -74,8 +87,7 @@ integration = website.integrations.create!(
7487
enabled: true
7588
)
7689

77-
# Retrieve the key to configure in SPP:
78-
integration.credential(:api_key)
90+
integration.credential('api_key')
7991
# => "a1b2c3d4e5f6..."
8092
```
8193

@@ -166,34 +178,20 @@ The API key grants the permissions of the website's owner/admin. This is appropr
166178

167179
### Audit Trail
168180

169-
PWB should log API key usage for auditing. The `api_manage` base controller could log:
170-
171-
```ruby
172-
def authenticate_from_api_key
173-
api_key = request.headers['X-API-Key']
174-
return nil if api_key.blank?
175-
176-
integration = current_website&.integrations&.find_by(api_key: api_key, active: true)
177-
if integration
178-
integration.touch(:last_used_at) # Already supported by the model
179-
# ... return acting user
180-
end
181-
end
182-
```
183-
184-
The `last_used_at` field already exists on `WebsiteIntegration`.
181+
The `api_manage` base controller calls `integration.record_usage!` on successful API key authentication, which updates `last_used_at` on the `WebsiteIntegration` record. This is already implemented.
185182

186183
## Enquiry Submissions: No Auth Needed
187184

188185
The enquiry endpoint (`POST /api_public/v1/enquiries`) is under `api_public`, which has **no authentication requirement**. This is correct — enquiries are submitted by anonymous visitors. SPP's client-side form can POST directly to PWB with just `X-Website-Slug` (no API key).
189186

190187
## Implementation Checklist
191188

192-
1. Ensure the `api_manage` base controller's `authenticate_from_api_key` method works with `WebsiteIntegration` records
193-
2. Create an SPP integration for each tenant that uses SPP
194-
3. Provide the API key and website slug to SPP's deployment configuration
195-
4. Verify that `api_manage` endpoints return 401 without a valid key and 200 with one
196-
5. Confirm `last_used_at` is updated on successful authentication
189+
- [x] `authenticate_from_api_key` iterates encrypted `WebsiteIntegration` credentials
190+
- [x] `:spp` category added to `WebsiteIntegration::CATEGORIES`
191+
- [x] Provisioning rake task: `rails spp:provision[subdomain]`
192+
- [x] API key auth returns 401 without valid key, 200 with one (6 specs)
193+
- [x] `record_usage!` updates `last_used_at` on authentication
194+
- [x] Cross-website key isolation tested
197195

198196
## Reference Files
199197

docs/api/spp/cors.md

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# SPP CORS Configuration (Option B)
22

3-
**Status:** Proposed
3+
**Status:** Implemented (Approach A — regex patterns)
44
**Related:** [SPP–PWB Integration](./README.md) | [Authentication](./authentication.md)
55

66
---
@@ -21,9 +21,9 @@ All blocks use `headers: :any`, which already permits custom headers like `X-Web
2121

2222
## What Needs to Change
2323

24-
### Development: Already Covered
24+
### Development: Covered
2525

26-
`localhost:4322` is already in the development origins block. If SPP runs on a different port locally, add it to the same block.
26+
`localhost:4321`, `localhost:4322`, and `localhost:4323` are in the development origins block. Port 4323 was added for SPP local development.
2727

2828
### Production: Add Per-Tenant SPP Origins
2929

@@ -83,7 +83,7 @@ end
8383
| Performance | No DB lookup | Cached DB lookup (5-min TTL) |
8484
| Security | Allows all matching domains | Only allows configured domains |
8585

86-
**Recommendation:** Start with Approach A. Move to Approach B only if tenants need fully custom SPP domains.
86+
**Decision:** Approach A was implemented. Move to Approach B only if tenants need fully custom SPP domains.
8787

8888
### Scoping: Which Resources?
8989

@@ -124,11 +124,12 @@ SPP uses API key authentication (not cookies), so `credentials: true` is **not n
124124

125125
## Implementation Checklist
126126

127-
1. Decide on Approach A or B based on whether tenants will have custom SPP domains
128-
2. Update `config/initializers/cors.rb` with the chosen approach
129-
3. Add `max_age: 3600` to the production origins block
130-
4. Test preflight (`OPTIONS`) requests from an SPP origin to both `/api_public/v1/*` and `/api_manage/v1/*`
131-
5. Verify `X-Website-Slug` and `X-API-Key` headers pass through
127+
- [x] Approach A (regex patterns) implemented in `config/initializers/cors.rb`
128+
- [x] Added `/.*\.spp\.propertywebbuilder\.com/` origin pattern
129+
- [x] Added `max_age: 3600` to the production origins block
130+
- [x] Added `localhost:4323` for SPP local dev
131+
- [ ] Test preflight (`OPTIONS`) from an SPP origin (manual/E2E)
132+
- [ ] Verify `X-Website-Slug` and `X-API-Key` headers pass through (manual/E2E)
132133

133134
## Reference Files
134135

docs/api/spp/data-freshness.md

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# SPP Data Freshness Strategy
22

3-
**Status:** Proposed
3+
**Status:** Phase 1 Implemented (cache headers), Phase 2 planned (webhooks)
44
**Related:** [SPP–PWB Integration](./README.md) | [SppListing Model](./spp-listing-model.md)
55

66
---
@@ -35,15 +35,15 @@ These callbacks are the natural hook points for notifying SPP.
3535

3636
### Current API Caching
3737

38-
`ApiPublic::V1::BaseController` sets `expires_in 5.hours, public: true` on all responses. SPP can use these cache headers if 5-hour staleness is acceptable.
38+
`ApiPublic::V1::BaseController` sets `expires_in 1.hour, public: true` on all responses (reduced from 5 hours as part of Phase 1).
3939

4040
## Strategy Evaluation
4141

4242
### Option 1: Cache Headers (Passive)
4343

4444
**How:** SPP respects the `Cache-Control` and `Last-Modified` headers from PWB's API. Astro's server-side data fetching can cache responses and re-validate with conditional GET (`If-Modified-Since`).
4545

46-
**Staleness:** Up to 5 hours (current `expires_in` value). Could be reduced to 1 hour for property detail endpoints.
46+
**Staleness:** Up to 1 hour (current `expires_in` value after Phase 1 reduction).
4747

4848
**Pros:**
4949
- Zero implementation on PWB — already works
@@ -183,16 +183,16 @@ These callbacks are the natural hook points for notifying SPP.
183183

184184
## Recommended Approach: Phase 1 Now, Phase 2 Later
185185

186-
### Phase 1: Reduce Cache TTL (Immediate)
186+
### Phase 1: Reduce Cache TTL (Done)
187187

188-
Reduce the `api_public` cache TTL for property detail endpoints from 5 hours to 1 hour:
188+
The `api_public` base controller cache TTL was reduced from 5 hours to 1 hour:
189189

190190
```ruby
191-
# In the properties show endpoint:
191+
# app/controllers/api_public/v1/base_controller.rb
192192
expires_in 1.hour, public: true
193193
```
194194

195-
This is a one-line change. SPP will see updates within an hour with no new infrastructure.
195+
SPP sees updates within an hour with no new infrastructure. Covered by 3 specs in `spec/requests/api_public/v1/cache_headers_spec.rb`.
196196

197197
### Phase 2: Add Webhooks (When Real-Time Matters)
198198

@@ -212,10 +212,11 @@ SPP can choose which events to act on. For most cases, `property.updated` as a c
212212

213213
## Implementation Checklist
214214

215-
### Phase 1 (Now)
216-
1. Reduce cache TTL on property detail API endpoint to 1 hour
217-
2. Ensure `Last-Modified` headers are set correctly on property responses
218-
3. SPP: Use conditional GET (`If-Modified-Since`) to avoid re-downloading unchanged data
215+
### Phase 1 (Done)
216+
- [x] Reduced cache TTL on all `api_public` endpoints to 1 hour
217+
- [x] `Vary` header set for `Accept-Language, X-Website-Slug` for proper edge caching
218+
- [x] Conditional GET (`If-Modified-Since`) supported via `fresh_when` in base controller
219+
- [ ] SPP: Use conditional GET headers to avoid re-downloading unchanged data
219220

220221
### Phase 2 (Later)
221222
1. Add `spp_webhook_url` and `spp_webhook_secret` to `client_theme_config` schema

0 commit comments

Comments
 (0)