Repository navigation
Description
Summary
sydevs/SahajCloud#801 replaced the widget's two write endpoints with one unified intake, and this repo was never updated. createRegistration still posts to POST /api/events/{id}/register, deleted with src/collections/Events/endpoints/registerForEvent.ts. sendUserMessage still posts to POST /api/user-messages, whose collection was deleted in the same PR. Event registration and the issue-report form both fail against cloud.sydevelopers.com.
Approach
What replaced them. One collection takes every public write, discriminated by type — contact, subscribe, registration, proposal. Registration is type: 'registration'. An issue report is type: 'contact'. The Turnstile header does not change: the write guard runs above the collection, before anything reads type.
Both call sites are in src/config/api/mutate.ts:
| line | sends now | send instead |
|---|---|---|
| 138 | POST /events/{id}/register |
POST /user-submissions, type: 'registration' |
| 288 | POST /user-messages |
POST /user-submissions, type: 'contact' |
⚠ The two halves are not equally ready — the report half is blocked
A contact submission must name a form, and this widget has no way to obtain one. needsForm (SahajCloud src/collections/UserSubmissions/fields.ts:175) returns true for every contact row — the one exemption covers subscribe only — and perTypeForm marks the relationship required under that condition, which Payload evaluates server-side on the REST path. A contact create with no form is refused by plain required validation, with no machine-readable code to map.
The widget cannot supply one:
- The
sahaj-atlasproject grantsregions,events,images,files(SahajCloudsrc/plugins/access/config/projects.ts:83).formssits inwemeditate-webonly, so the atlas key cannot read or list it. sy-atlas-configcarriescanonicalFallbackClient,defaultMapCenter,defaultZoomLevel— no form reference.prepareUserSubmissionthen cross-checksform.actionTypeagainsttypeand refusessubmission_type_mismatch, so any id would also have to be acontactform.
So the registration half is mechanical and shippable now; the report half needs a SahajCloud decision first. See ## Open questions. Do not open a PR that half-lands the report path.
Field mapping — columns vs submissionData pairs
submissionData is a [{ field, value }] array, not a flat body. Real columns stay columns. SahajCloud src/collections/UserSubmissions/submissionData.ts is the authority.
Registration (mutate.ts:136):
| widget value | goes to |
|---|---|
eventId |
event relationship column |
data.email |
senderEmail column |
data.startingAt |
startingAt column (timezone-aware date) |
data.name |
submissionData pair, key name |
activeLocale() |
submissionData pair, key locale |
data.questions |
one pair per entry — Object.entries |
The question keys already line up. RegistrationQuestionNameSchema (src/types/event.ts) and SahajCloud's EVENT_REGISTRATION_QUESTIONS name the same four: experience, referral, aspirations, questions. No renaming needed — TYPE_SUBMISSION_KEYS.registration is built from that same list.
Report (mutate.ts:259): message and subject become pairs; senderEmail stays a column; the four context keys flatten to pairs under their existing names.
⚠ Those key names are load-bearing. URL_EXEMPT_KEYS is exactly path, hostUrl, error, userAgent, locale. The write guard's URL scan walks every other pair, so renaming one — or sending pageUrl instead of hostUrl — starts refusing ordinary bug reports with urls_not_allowed. mutate.ts:267 already does this rename; keep it.
New bounds to respect
checkSubmissionData adds bounds the flat body never had:
- At most 40 pairs per submission (
MAX_SUBMISSION_DATA_ENTRIES). - Key length ≤ 100 (
MAX_SUBMISSION_DATA_KEY_LENGTH). - Value ≤ 2000 by default, but 5000 for
message,noteanderror(VALUE_MAX_LENGTHS). - A duplicate key, a non-string value, or a key the type does not accept →
submission_data_invalid(400). The message names the offending key.
The widget's uniform CONTEXT_MAX = 2000 (mutate.ts:235) stays correct and is now merely stricter than the server for error. Leave it — a truncated error string is a better trade than a refused report.
The success shape changes — but nothing reads it
RegistrationResponseSchema (mutate.ts:12) expects { ok: true, registration: { id, uuid } }. The intake returns Payload's 201 create envelope. No consumer reads any of it: RegistrationForm.tsx:190 is onSuccess: () => setSubmitted(true). So narrow the schema to { doc: { id: z.number() } }, matching UserMessageResponseSchema (mutate.ts:178) and for the same stated reason — id is the one field a CMS rename cannot touch.
The report path's schema needs no change at all.
Refusal codes
gateRegistration.ts throws a Payload APIError carrying code at errors[].data.code. asRefusal (mutate.ts:95) already reads both positions, so it keeps working untouched.
⚠ event_not_found will NOT arrive from pnpm types:cms. SahajCloud's responseTypes.ts:71 lists four codes and its own JSDoc states "the plain not-found 404 carries no code" — while gateRegistration.ts:61 throws { code: 'event_not_found' } on that very 404. The gate is what actually runs. Add the code to the local union the way captcha_failed already is, with a comment saying why, and raise the contradiction upstream.
Two new 400 codes to map: submission_data_invalid and submission_type_mismatch. Both are integrator errors, not viewer-actionable, so they belong in the generic-failure path rather than getting their own copy.
The synced types
src/types/payload/payload-types.ts still declares registrations, event-submissions, user-messages and form-submissions, and has no user-submissions. Run pnpm types:cms in the same PR.
⚠ It also drops EventRegistrationResponse from response-types.ts — that export is gone upstream. Nothing here imports it (only EventRegistrationErrorCode, in mutate.ts:1 and RegistrationForm.tsx:3), so this is safe. Confirm with a grep rather than assuming; docs/rules/data-layer.md warns that a curled URL can keep resolving while serving something else.
Docs and specs in the same PR
docs/rules/data-layer.mdnames both endpoints twice — in the fetchers preamble ("SahajCloud exposes only raw collection reads and a few custom endpoints …POST /api/events/:id/register") and in the whole Mutations section. Both are now wrong.src/config/api/mutate.test.tspins the old paths (createRegistrationdescribe block, from line 47). Update its assertions to the new path,type, and pair shape.- Two comments name deleted paths:
mutate.ts:230cites SahajCloud'ssrc/collections/UserMessages/UserMessages.tsfor the 2000-character bound, which now lives insrc/collections/UserSubmissions/submissionData.ts(and is per-key, not uniform);ReportIssueForm.tsx:83names the old endpoint. docs/embedding.mdneeds no change — the origin is unchanged, so the host CSP table is unaffected.
Acceptance criteria
- No request in
src/targets/events/{id}/registeror/user-messages. - A registration submits as a
type: 'registration'create on/api/user-submissionsand reaches its confirmation screen. -
event,senderEmailandstartingAtare sent as top-level fields, andname,localeand each enabled question assubmissionDatapairs. - The report's context pairs keep the keys
path,hostUrl,userAgent,locale,error— the setURL_EXEMPT_KEYSnames. - Both requests still send
x-turnstile-token. -
external_registration,event_ended,registration_closed,event_fullandevent_not_foundeach render refusal copy. -
submission_data_invalidandsubmission_type_mismatchfall through to the generic failure message, not a blank screen. -
src/types/payload/payload-types.tsmatches SahajCloudmain, and nothing imports a type the re-sync removed. -
docs/rules/data-layer.md's Mutations section describes the intake, not the two deleted endpoints. - The comments at
mutate.ts:230andReportIssueForm.tsx:83name paths that exist.
Verification checklist
-
pnpm types:cms && git diff --stat src/types/payload/shows the re-sync. -
grep -rn "EventRegistrationResponse" src/returns nothing outsidesrc/types/payload/. -
grep -rnE "events/.+/register|'/user-messages'" src/ docs/returns nothing. -
pnpm lint && pnpm typecheck && pnpm test:runall clean. -
pnpm test:run src/config/api/mutate.test.ts— the registration spec asserts the new path and the pair shape, and fails when reverted to the old body. - Against the deployed Cloudflare preview, submit the registration form on an open event. Confirm a 201, and a row at
/admin/collections/user-submissionswithtype: registration, the four question answers assubmissionDatapairs, andsenderEmailset. - Register for an event whose last date has passed. Confirm the 409 renders the "this event has ended" copy.
- Register for an event id that does not exist. Confirm the 404 renders refusal copy, not the generic error.
- Submit a registration with a 3000-character answer. Confirm the 400 does not blank the form.
Open questions
- Where does the report form get its
contactform id? Three options, and the choice is a SahajCloud change either way: (a) add a form reference to thesy-atlas-configglobal, which the widget already reads; (b) exempt a client-originatedcontactrow fromneedsFormand fall back to the default recipient; (c) publish the id as a build-timeVITE_variable, which drifts silently and is the weakest of the three. (a) looks right — the widget already boots off that global and the id is not a secret. - Does the registration confirmation copy still hold? The deleted endpoint composed and sent the confirmation inline. The intake returns
status: 'pending'and delivery is a queued job with no callback, so the widget is never told the outcome — the same narrowingreport.sentwas reworded for in fix(event): deliver report-issue submissions instead of alerting #103. If the copy must change, it is CMS-owned: that needs an issue onsydevs/SahajCloudagainstsy-atlas-translations, not an edit here.
Downstream impact
None outbound. This repo is the consumer; SahajCloud already shipped its half in #801. The two open questions above are inbound asks on SahajCloud.
Notes
Found by Thursday's survey-contracts sweep.
Evidence: sydevs/SahajCloud#801 deletes src/collections/Events/endpoints/registerForEvent.ts and src/collections/UserMessages/UserMessages.ts. src/collections/Events/endpoints/ now holds only geojson.ts, responseTypes.ts and verifyEventAction.ts. grep -rn "user-submissions" src/ in this repo returns nothing.
Verified against SahajCloud main at 07e3e40, reading the collection source rather than the API. GET https://cloud.sydevelopers.com/api/openapi.json needs a client key and returned 401.
sydevs/SahajCloud#796 covered the server-side port only. No ticket tracks the consumers.
🤖 Written by the sydevs autonomous loop — see what this is
Metadata
Metadata
Assignees
Labels
Type
Fields
Priority
Hold Until
Projects
- StatusShow more project fieldsDone
Activity
sydevs-bot commentedon Sep 17, 2026
🤖 no session running
recent dispatches
Written by the sydevs dispatcher. Sessions never edit this comment.
sydevs-bot commentedon Sep 17, 2026
The report half of this ticket is blocked; the registration half is not. A
contactsubmission must name aformsid, and the atlas key cannot readforms— its project grantsregions,events,images,filesonly, andsy-atlas-configcarries no form reference. Added as an open question with three options.Other corrections from reading the code:
submissionDatapairs.event_not_foundcontradicts SahajCloud'sresponseTypes.ts, which says a 404 carries no code. The gate throws one anyway.URL_EXEMPT_KEYSconstraint, the newsubmissionDatabounds,docs/rules/data-layer.md, andmutate.test.ts.Effort stays Hard.
This is arguably two tickets — registration now, report once the form question is answered. Comment
@sydevs-bot splitto separate them.@sydevs-bot implementstarts work.🤖 Written by the sydevs autonomous loop — see what this is
Generated by Claude Code
Ardnived commentedon Sep 18, 2026
@sydevs-bot This ticket appears to be redundant with an already existing ticket: #213
If that's true, close this ticket and log this to the ops journal. We should make sure that the daily surveys don't duplicate existing tickets.
sydevs-bot commentedon Sep 18, 2026
Confirmed redundant. Closing this as a duplicate of #195.
#195 specifies the same migration and was filed 2026-09-04. PR #213 implements it and opened 2026-09-17 01:34Z — seven hours before this ticket was filed. That PR changes every file named here:
src/config/api/mutate.ts,mutate.test.ts,docs/rules/data-layer.md,src/types/payload/.Nothing here is lost by closing:
contactrow needs aformsid the atlas key cannot read — is tracked as feat(sy-atlas-config): name the atlas report-issue form on the config global, and openformsto the atlas scope SahajCloud#813 and named in feat(api): migrate widget intakes to unified form-submissions #213's body.submissionDatabounds and theevent_not_foundbehaviour are all in feat(api): migrate widget intakes to unified form-submissions #213.This ticket's claim "No ticket tracks the consumers" was wrong. The survey read SahajCloud, not this repo's open work.
Logged the survey gap to the ops journal, sydevs/claude-workflow#111. A dedupe step for the survey skills needs a
claude-workflowPR, which I have not opened.🤖 Written by the sydevs autonomous loop — see what this is
Generated by Claude Code