DEWP Setup Fixes — Required Actions
Issued 5 August 2026 · Based on test push failure (HTTP 500: "You must be logged in to access this app"). These are the exact changes the DEWP app builder needs to make.
Push test passed — 5 Aug 2026
Both test users (Test UserOne, Test UserTwo) were pushed successfully. The DEWP returnedstatus: success, action: createdfor both, with ApprovedSubscriber records created at access_state: Active. CRM records auto-updated: dewp_pushed=true, dewp_status=Active, master status=Active, dewp_user_id stored.
Status sync (get_status) — DEWP-side bug found
The CRM's syncDEWPStatuses function sends{ action: "get_status", emails: ["a@b.com", "c@d.com"] }— emails is an array of plain strings. The DEWP responded with:
HTTP 500: "Cannot read properties of undefined (reading 'email')"
Fix on the DEWP side: In the get_status branch of receiveUserPush, make sure the code treats body.emails as an array of strings, not objects:
// ❌ Wrong — treats each entry as an object with .email const normalized = body.emails.map(e => e.email.toLowerCase()); // ✅ Correct — each entry is already a plain string const normalized = body.emails.map(e => e.toLowerCase());
This is not blocking — the push path works perfectly. Status sync is for ongoing reconciliation (confirming the DEWP's state matches the CRM). Fix it when convenient, then re-run "Sync DEWP Statuses" from the CRM to verify.
Where does the DEWP store pushed users?
The CRM pushes users to the ApprovedSubscriber entity — this is a separate entity from the built-in User entity. They are two different things:
| ApprovedSubscriber entity | User entity (built-in) |
|---|---|
| The approval list — who is allowed to access the platform. | The login account — the actual credentials a person uses to sign in. |
| Created by the CRM push (server-to-server, via API key). | Created by the user self-signing up on the DEWP (email + password). |
| View in: Dashboard → Entities → ApprovedSubscriber (or Data Management). | View in: Dashboard → Users. |
| Fields: email, first_name, last_name, organisation, crm_reference, source_type, access_state (Active/Inactive). | Built-in: id, email, full_name, role, created_date. |
So you will NOT see pushed users under Dashboard → Users. That section only shows people who have already created their own login account. Pushed users appear under the ApprovedSubscriber entity in the data/entities section.
The full flow (how it should operate):
- CRM pushes user (e.g. testuser1@testcompany.co.nz) → DEWP creates an ApprovedSubscriber record with
access_state: Active. No User account exists yet. - User self-signs up on the DEWP with that email + a password → the platform creates a User account (appears under Dashboard → Users). No Base44 invite email is sent.
- User logs in → the DEWP's AuthContext queries ApprovedSubscriber by the user's email. If a record exists with
access_state: Active→ access granted. If not found, orInactive→ access denied. - CRM revokes access (pushes with
access_state: Inactive) → the ApprovedSubscriber record flips toInactive. Next time the user logs in, AuthContext blocks them. Their User account still exists, but they can't see content. - CRM re-grants access (pushes with
access_state: Active) → the ApprovedSubscriber record flips back toActive. User logs in and content is visible again.
Key point for the DEWP builder: Do NOT create User accounts from the push. The push only manages the approval list (ApprovedSubscriber). The user creates their own account via self-signup. The two are linked by email address. This is why pushed users won't appear under Dashboard → Users until the person self-signs up.
What went wrong in the test
The CRM pushed a test user (SUB-BUS-26-TEST01, Test UserOne) to the DEWP with a validx-api-keyheader. The DEWP's receiveUserPush function calledbase44.auth.me()and rejected the request because there is no logged-in user session — server-to-server pushes have no user session. The function must authenticate via the API key alone.
receiveUserPush — accept API-key only (no user login)
Blocker — push fails without thisThis is the fix that unblocks the push. Open the DEWP's receiveUserPush backend function and apply these changes:
❌ Remove this (requires user session):
const base44 = createClientFromRequest(req);
const user = await base44.auth.me();
if (!user) return Response.json({ error: 'Unauthorized' }, { status: 401 });
if (user.role !== 'admin') return Response.json({ error: 'Forbidden' }, { status: 403 });✅ Replace with this (API-key validation):
import { createClientFromRequest } from 'npm:@base44/sdk';
import { secrets } from 'base44:runtime';
export default async function(req: Request): Promise<Response> {
const base44 = createClientFromRequest(req);
// ── Authenticate via API key ONLY (no user session) ──
const apiKey = req.headers.get('x-api-key');
const expectedKey = secrets.get('DEWP_API_KEY');
if (!apiKey || apiKey !== expectedKey) {
return Response.json(
{ status: 'error', message: 'Invalid or missing API key' },
{ status: 401 }
);
}
const body = await req.json();
const { email, first_name, last_name, organisation,
crm_reference, access_state, source_type } = body;
if (!email) {
return Response.json({ status: 'error', message: 'email is required' }, { status: 400 });
}
// ── Use asServiceRole for ALL entity operations ──
// (no user session exists in a server-to-server push)
const grantAccess = (access_state || 'Active').toLowerCase() === 'active';
// Upsert the ApprovedSubscriber record
const existing = await base44.asServiceRole.entities.ApprovedSubscriber
.filter({ email: email.toLowerCase() }, '-created_date', 5);
let record;
if (existing.length > 0) {
record = await base44.asServiceRole.entities.ApprovedSubscriber
.update(existing[0].id, {
access_state: grantAccess ? 'Active' : 'Inactive',
crm_reference: crm_reference || existing[0].crm_reference,
first_name: first_name || existing[0].first_name,
last_name: last_name || existing[0].last_name,
organisation: organisation || existing[0].organisation,
source_type: source_type || existing[0].source_type,
});
} else {
record = await base44.asServiceRole.entities.ApprovedSubscriber.create({
email: email.toLowerCase(),
first_name, last_name, organisation,
crm_reference: crm_reference || '',
source_type: source_type || 'subscription',
access_state: grantAccess ? 'Active' : 'Inactive',
});
}
// ── DO NOT call base44.users.inviteUser ──
// Self-signup handles account creation; the approval list + RLS controls access.
// Calling inviteUser would send an unwanted Base44 invite email.
return Response.json({
status: 'success',
action: existing.length > 0 ? 'updated' : 'created',
user_id: record.id,
access_state: grantAccess ? 'Active' : 'Inactive',
});
}The DEWP must have a secret named DEWP_API_KEY set to the same value as the CRM's DEWP_API_KEY secret.
get_status — report access state for a batch of emails
Blocker — push fails without thisThe CRM's syncDEWPStatuses function calls the DEWP with { action: "get_status", emails: [...] } and expects a reply listing each user's current access state. Apply the same API-key authentication, then return the statuses:
export default async function(req: Request): Promise<Response> {
const base44 = createClientFromRequest(req);
const apiKey = req.headers.get('x-api-key');
const expectedKey = secrets.get('DEWP_API_KEY');
if (!apiKey || apiKey !== expectedKey) {
return Response.json({ status: 'error', message: 'Invalid API key' }, { status: 401 });
}
const body = await req.json();
if (body.action !== 'get_status' || !Array.isArray(body.emails)) {
return Response.json({ status: 'error', message: 'action=get_status and emails[] required' }, { status: 400 });
}
const normalized = body.emails.map(e => e.toLowerCase());
// Fetch all matching ApprovedSubscriber records (service role — no user session)
const records = await base44.asServiceRole.entities.ApprovedSubscriber
.filter({ email: { $in: normalized } }, '-created_date', 200);
// Build a lookup keyed by email
const map = {};
for (const r of records) { map[r.email] = r; }
const users = body.emails.map(email => {
const r = map[email.toLowerCase()];
return {
email,
user_id: r ? r.id : null,
status: r ? r.access_state : 'not_found', // Active | Inactive | not_found
};
});
return Response.json({ status: 'success', users });
}ApprovedSubscriber entity + RLS gate on all content
Critical — security gate3a. Create the ApprovedSubscriber entity with these fields:
{
"name": "ApprovedSubscriber",
"type": "object",
"properties": {
"email": { "type": "string", "description": "Lowercase login email" },
"first_name": { "type": "string" },
"last_name": { "type": "string" },
"organisation": { "type": "string" },
"crm_reference": { "type": "string", "description": "CRM subscription reference" },
"source_type": { "type": "string", "enum": ["subscription", "trial"] },
"access_state": { "type": "string", "enum": ["Active", "Inactive"], "default": "Active" }
},
"required": ["email", "access_state"],
"rls": {
"create": null,
"read": { "user_condition": { "role": "admin" } },
"update": { "user_condition": { "role": "admin" } },
"delete": { "user_condition": { "role": "admin" } }
}
}RLS note: create: null allows the service-role push to insert records without a user session. Reads/updates/deletes are admin-only so regular users cannot tamper with the approval list.
3b. Gate every wellness content entity with RLS. Add this read rule to each entity that holds platform content (assessments, course modules, collective posts, resources, etc.):
"rls": {
"read": {
"user_condition": {
"$exists_approved_subscriber": {
"entity": "ApprovedSubscriber",
"match": { "email": "{{user.email}}", "access_state": "Active" }
}
}
},
"create": { "user_condition": { "role": "admin" } },
"update": { "user_condition": { "role": "admin" } },
"delete": { "user_condition": { "role": "admin" } }
}This is the forwarded-link protection. A stranger who self-signs up with an unknown email passes authentication (they have a valid account) but fails this RLS check on every content query — they see nothing. Only users whose email matches an ApprovedSubscriber withaccess_state: Active can read content.
If the RLS syntax above isn't supported in your Base44 version, the alternative is a shared backend function that content pages call instead of direct entity reads — the function checks ApprovedSubscriber via asServiceRole and returns content only if the user is approved.
App settings — Public (Require login) + Email/Password only
Critical — security gateIn the DEWP app's settings:
- Visibility: Public (Require login) — anyone with the link can create their own account.
- Authentication: Enable Email and password only. Disable all OAuth/social providers (Google, etc.).
- Do NOT enable "invite-only" mode — self-signup must be open so no Base44 invite email is ever generated.
reconcile — bidirectional audit endpoint (quality check)
Recommended — auditOnce fixes 1–4 are working, add this endpoint so the CRM can run a full reconciliation (not just status sync). It accepts a batch of { email, crm_reference } pairs from the CRM and reports what the DEWP has:
export default async function(req: Request): Promise<Response> {
const base44 = createClientFromRequest(req);
const apiKey = req.headers.get('x-api-key');
if (!apiKey || apiKey !== secrets.get('DEWP_API_KEY')) {
return Response.json({ status: 'error', message: 'Invalid API key' }, { status: 401 });
}
const body = await req.json();
if (body.action !== 'reconcile' || !Array.isArray(body.records)) {
return Response.json({ status: 'error', message: 'action=reconcile and records[] required' }, { status: 400 });
}
const emails = body.records.map(r => r.email.toLowerCase());
const dewpRecords = await base44.asServiceRole.entities.ApprovedSubscriber
.filter({ email: { $in: emails } }, '-created_date', 200);
const dewpMap = {};
for (const r of dewpRecords) { dewpMap[r.email] = r; }
// For each CRM record, report what DEWP has
const results = body.records.map(crmRec => {
const email = crmRec.email.toLowerCase();
const dewp = dewpMap[email];
return {
crm_email: crmRec.email,
crm_reference: crmRec.crm_reference,
dewp_found: !!dewp,
dewp_access_state: dewp ? dewp.access_state : null,
dewp_crm_reference: dewp ? dewp.crm_reference : null,
dewp_user_id: dewp ? dewp.id : null,
reference_match: dewp ? (dewp.crm_reference === crmRec.crm_reference) : null,
};
});
// Also report DEWP orphans (DEWP records not in the CRM batch)
const crmEmails = new Set(emails);
const orphans = dewpRecords
.filter(r => !crmEmails.has(r.email))
.map(r => ({ email: r.email, access_state: r.access_state, crm_reference: r.crm_reference }));
return Response.json({ status: 'success', results, orphans });
}The CRM calls this with its full active user list and flags: Missing (CRM Active, DEWP not found), Mismatch (states differ), Orphan (DEWP has a record the CRM doesn't), and Stale reference (reference numbers don't match).
ApprovedSubscriber list — show Active/Inactive badge for each user
Recommended — auditThe default entity list view doesn't visually flag a user's access state. The DEWP builder should create a dedicated ApprovedSubscriber management page (admin-only) that lists every pushed user with a clear coloured badge showing whether their access is Active or Inactive. This makes it easy to see at a glance who currently has platform access and who has been revoked.
Badge colours:
- Active — access granted (user can see content).
- Inactive — access revoked (user is blocked by RLS).
Example list row (React + Tailwind):
function AccessStateBadge({ state }) {
const isActive = (state || "").toLowerCase() === "active";
return (
<span className={`inline-flex items-center gap-1 text-xs font-medium px-2 py-0.5 rounded-full ${
isActive ? "bg-green-100 text-green-700" : "bg-gray-200 text-gray-600"
}`}>
{isActive ? "Active" : "Inactive"}
</span>
);
}
// Inside your ApprovedSubscriber list/table:
{subscribers.map(s => (
<tr key={s.id}>
<td>{s.first_name} {s.last_name}</td>
<td>{s.email}</td>
<td>{s.organisation}</td>
<td>{s.crm_reference}</td>
<td><AccessStateBadge state={s.access_state} /></td>
</tr>
))}Build this as an admin page (e.g. /admin/subscribers) that lists all ApprovedSubscriber records. Add a header summary showing the count of Active vs Inactive users, and optionally a filter to show only Active or only Inactive. The access_state field is the source of truth — it's what RLS checks on every content read, so the badge should always reflect its current value.
receiveUserPush — support access_state: 'Deleted' (hard delete)
Recommended — auditThe CRM can now send access_state: "Deleted" to permanently remove a user's ApprovedSubscriber record — used for test-user cleanup and full removal. Add a delete branch at the top of receiveUserPush (after the API-key check, before the upsert logic):
export default async function(req: Request): Promise<Response> {
const base44 = createClientFromRequest(req);
// ── API-key auth (same as fix #1) ──
const apiKey = req.headers.get('x-api-key');
if (!apiKey || apiKey !== secrets.get('DEWP_API_KEY')) {
return Response.json({ status: 'error', message: 'Invalid API key' }, { status: 401 });
}
const body = await req.json();
const { email, access_state } = body;
// ── DELETE branch: permanently remove the ApprovedSubscriber record ──
if ((access_state || '').toLowerCase() === 'deleted') {
if (!email) return Response.json({ status: 'error', message: 'email is required for delete' }, { status: 400 });
const existing = await base44.asServiceRole.entities.ApprovedSubscriber
.filter({ email: email.toLowerCase() }, '-created_date', 5);
if (existing.length > 0) {
await base44.asServiceRole.entities.ApprovedSubscriber.delete(existing[0].id);
return Response.json({ status: 'success', action: 'deleted', user_id: existing[0].id });
}
// Already gone — still report success so the CRM can clear its flags
return Response.json({ status: 'success', action: 'deleted', user_id: null });
}
// ── Normal upsert (Active / Inactive) continues here ──
// ... existing upsert code from fix #1 ...
}The CRM clears its own dewp_pushed, dewp_user_id, anddewp_status fields back to "Not Pushed" after a successful delete, so the user can be re-pushed later if needed. Trigger this from the per-user Delete button (red, trash icon) in the CRM subscription / trial user table.
When to use Delete vs Revoke:
- Revoke (Inactive) — default for end-of-subscription. Keeps the DEWP record as Inactive (audit trail); the user is blocked by RLS but can be re-granted.
- Delete (Deleted) — permanent removal for test users or cleanup. The ApprovedSubscriber record is gone; re-pushing creates a fresh record.
Verification checklist (after fixes are applied)
- CRM: re-push test user SUB-BUS-26-TEST01 / Test UserOne → DEWP returns
status: successwith auser_id. - DEWP: confirm an ApprovedSubscriber record exists with
access_state: Activefor that email. - DEWP: self-sign up with
testuser1@testcompany.co.nz+ any password → confirm content is visible (RLS passes). - Self-sign up with a random email → confirm content is not visible (RLS blocks).
- CRM: set Test UserOne to Inactive and re-push → DEWP
access_stateflips to Inactive. - DEWP: log in as Test UserOne → confirm content is now blocked (RLS fails on Inactive).
- CRM: run Sync DEWP Statuses → confirm
dewp_statusupdates to match the DEWP's reported state. - Optional: CRM calls the reconcile endpoint → confirm no mismatches or orphans for the test group.
Test data: subscription SUB-BUS-26-TEST01 under "Test Company Ltd", users Test UserOne and Test UserTwo. Delete these from the CRM after verification is complete.
