For app vendors and EduSpaze

Opening apps inside EduSpaze

Opening apps inside EduSpaze

How pupils and staff open an app at its address on EduSpaze, when it opens inside an EduSpaze page and when in a tab of its own, how EduSpaze proves it can, and what is measured while it is open. Decided 2026-10-22.

1. The address

Every app has one: https://www.eduspazeconnect.com/app/access/<slug> — for Zoala, /app/access/zoala.

  • The slug is made from the product name when the vendor is created (lower case, anything else becomes a hyphen, at most 50 characters; -2, -3 … when taken). A database trigger (startup_slug_fill) makes it, so nothing that creates a vendor can forget it; slugBase() in lib/apps/slug.ts is the same rule in TypeScript, and a test holds them equal.
  • Only a platform admin changes it (Admin → Vendors → the vendor → *Opening inside EduSpaze*, platform.engagement EDIT). The vendor sees it, read-only, on Integration & API.
  • An old address keeps working. It is kept (StartupSlug) and redirects to the new one, and it is never given to another app — it still leads to the first. An app may take back one of its own former addresses. Capitals redirect too: /app/access/Zoala → /app/access/zoala.
  • The address carries nothing but the app. Who is opening it comes from their session.

2. Opening

The pupil and teacher launchers' Open goes to the address, in a new tab each time (?from=launcher, so the launch is recorded as from the launcher). Anyone may also type or bookmark it.

  1. Who are you? Not signed in: the school sign-in, then back. Anyone but a pupil or school staff: /no-access.
  2. Which app? The slug, or a former one (redirect). Unknown: not found.
  3. May you open it? Exactly the questions /open asks, through the same launch (lib/apps/launch.ts): a pupil must be connected to the app; a member of staff must hold school.apps EDIT and the school must have opened the app to teachers (seats and activation included). Refusals go where /open's go. The vendor's handshake does not change — the same signed launch, sign-in link or Sign in with EduSpaze (docs/integration/vendor.md §3, §10–§12).
  4. Inside or its own tab? Where the launch lands on the vendor's site, it is shown inside the page if all of these hold (shouldFrame, lib/apps/frame.ts); otherwise this tab goes on to the app, as /open does:
  5. the platform module is on — Admin → Settings → Modules → Open apps inside EduSpaze (app_frame, on);
  6. the app is switched on — the vendor's *Opening inside EduSpaze* page (frameEnabled, on);
  7. the test passed for this audience — pupils or staff (§3). Each goes by its own pages, so a pupil site can open inside while the staff site cannot. A missing or week-old test is run again, briefly, on the next open; while an address change is untested nobody opens inside;
  8. the launch lands on a site that audience's test covered. A launch that goes somewhere else first — Google's or Microsoft's sign-in, which never allow it — opens in its own tab, and so does the app's own SSO start (ssoUrl), which goes straight on to one.
  9. Sign-in help, inside. Where opening would first show EduSpaze's sign-in help (an app that signs people in on its own page, with no sign-in link — docs/vendor-sign-in.md §5), and the audience's test passed, the frame opens on the app's own sign-in page instead, with the help in a strip under the bar: the account to use, with *Copy*; the password, when the school linked one, hidden until *Show*; one line on which account to use. It is open at first and closes with ✕; *Your sign-in* in the bar brings it back. Otherwise the help page is shown as before and the app opens in its own tab.

The page is a slim bar — *My apps*, the app's icon and name, *Not loading? Open in its own tab* — and the app filling the window. The frame grants fullscreen; clipboard-write; autoplay; encrypted-media and nothing else: no camera, microphone or location for an app inside EduSpaze. The vendor's address is not in the address bar. It is in the page (anyone with the browser's developer tools can find it); it is not hidden, only not shown.

3. Proving it can — the test and the copilot

A site decides whether another site may show it, with two response headers: X-Frame-Options and the Content-Security-Policy directive frame-ancestors. And a browser drops cookies that are not SameSite=None inside another site's page, so a sign-in that relies on one fails there.

Run test opens every address opening the app can land on — its launch URL and its sign-in pages for pupils and staff — the way a browser would, following each redirect, and reads every page's headers. No account is used; no page content is kept.

Each audience is judged on its own pages: pupils on the launch URL and the pupils' sign-in page, staff on the launch URL and the staff sign-in page (Startup.frameStudent, frameStaff). Who opens inside is decided by these. The tag shows the worse of the two — something is still the vendor's to fix — and the card says who opens inside: *Pupils: opens inside · Staff: own tab*, with the note naming the page and cookie that keep the other out.

FindingResultTag
Every page allows EduSpaze; no sign-in cookie would be droppedpassesVerified
A page sends X-Frame-Options: DENY / SAMEORIGIN, or a frame-ancestors that does not name EduSpazefailsError
A cookie whose name says sign-in (session, sid, auth, token, xsrf/csrf, jwt, login, remember) is not SameSite=NonefailsError
Another cookie is not SameSite=Nonepasses, with a warningVerified
A page could not be reachedfailsError
Never tested, or the launch URL or a sign-in page address changed since—Unverified

frame-ancestors, where present, decides — browsers then ignore X-Frame-Options; every policy that sets it must allow EduSpaze; report-only policies decide nothing.

Check site — the copilot (frame.copilot in lib/ai/registry.ts) — reads the same findings and explains in plain words whether the app can open inside EduSpaze, which page and header decide it, and the one change the vendor would make. It is told each page's address, status and headers verdict and the cookies' names and SameSite policy; never page content, accounts or anything about a pupil or school. It explains; only the test sets the tag.

Both are on the vendor's *Opening inside EduSpaze* page (EduSpaze, platform.engagement EDIT) and on the vendor's own Integration & API page (vendor.integration EDIT) — the same card and panels as sign-in pages (docs/ui-ux-spec.md §7.18). The vendor list shows each app's tag, or Off, and filters by it.

What a vendor changes (also on its Integration page):

Content-Security-Policy: frame-ancestors https://www.eduspazeconnect.com
Set-Cookie: <session>=…; Secure; SameSite=None

and no X-Frame-Options: DENY or SAMEORIGIN. *Example, 2026-10-22: Zoala's pupil site (www.student.zoala.co) allows EduSpaze and keeps its sign-in in the page rather than in a cookie, so pupils open inside. Its staff site (app.zoala.co) allows EduSpaze too, but zoala_session and XSRF-TOKEN are SameSite=Lax, so staff open in their own tab until those are SameSite=None; Secure. Until 2026-10-22 one result covered both audiences, and the staff site's cookies kept pupils out as well.*

4. What is measured

While the app is open inside EduSpaze, the page's heartbeat (components/engagement/heartbeat.tsx) reports three states (docs/engagement-tracking.md §9):

  • Active — the tab is in front and the browser window focused. Clicking about inside the app counts: the page has focus while anything in it does.
  • Idle — still open, but in the background or the window unfocused. Kept apart, never counted as use. After the school's inactivity cutoff of idle the page stops reporting.
  • Stopped — the page was left or closed.

For pupils and staff alike; staff in tables of their own (StaffEngagementSession), so a teacher is never counted in pupil reporting or a parent's timeline. Schools see it under Engagement (Pupils and Staff tabs) and on a pupil's record. Only time is measured — never what anyone did inside the app. An app opened in its own tab is measured as before: the launch, and whatever the vendor reports.

5. Permissions (docs/rbac-spec.md §10)

QuestionAnswer
ModuleOpening: the launch's own — a pupil's connection; staff school.apps EDIT. Address, switch, test: platform.engagement (EDIT changes; anyone with platform.vendors VIEW sees the page). The vendor's check and test: vendor.integration EDIT. Staff time: school.engagement VIEW
Page call/app/access/<slug>: the session first, then the role, then the launch's own checks. /admin/vendors/<id>/opening: platform.vendors VIEW
VIEW without EDITThe opening page shows the address, the switch (not usable), the tag and the last note; no copilot or test
RolePlatform admin and support hold platform.engagement EDIT; a vendor owner holds vendor.integration EDIT

Source: docs/integration/app-access.md in the repository. Last updated with the code it describes.