Building your app
Extending the student profile
An embedded app can add content to okta-web's student-profile page straight from code — no portal forms. Write a Livewire component in a fixed folder, annotate it with a single attribute, and the platform auto-discovers and registers it.
Embedded apps only. External apps surface their data over HTTP/webhooks, not through this registry.
Folder layout (shipped in the boilerplate)
Modules/<App>/app/StudentProfile/
├── Panels/ ← full cards (Panel)
├── Stats/ ← header-strip numbers (Stat)
└── Actions/ ← header buttons (Action)
Any annotated component under these folders is discovered and registered automatically at boot — there is no registration code to write.
Declaration: an attribute on the class
namespace Modules\MyApp\app\StudentProfile\Panels;
use App\Support\StudentProfile\Attributes\StudentPanel;
use App\Support\StudentProfile\StudentProfileZone;
use Livewire\Component;
#[StudentPanel(
title: 'Attendance summary',
permission: 'my_app.records.view',
order: 50,
zone: StudentProfileZone::Main,
)]
class AttendanceSummary extends Component
{
public string $studentHashid; // the only identifier passed — never a numeric id
public function render()
{
return view('my-app::student-profile.attendance-summary');
}
}
keyandcomponentare derived automatically from the class.- Only
title/label+permissionare required; the rest is optional.
Mandatory rules
- Use
studentHashidonly — never a numeric id. - Every contribution declares a
permissionin the<feature>.<resource>.<action>shape. - This surface is embedded-only.
The four types (attribute parameters)
1) #[StudentPanel] — a full card
| Param | Required | Description |
|---|---|---|
title |
✓ | Card title |
permission |
✓ | View permission |
order |
— | Order (default 100, lower first) |
zone |
— | StudentProfileZone::Main (wide column) or ::Sidebar |
icon |
— | Icon |
description |
— | Short description |
key |
— | Override the derived key |
2) #[StudentStat] — a header-strip number
| Param | Required | Description |
|---|---|---|
label |
✓ | Label |
permission |
✓ | View permission |
order |
— | Order |
icon |
— | Icon |
color |
— | primary/success/warning/danger/… |
The component itself is the live widget that renders the value.
3) #[StudentAction] — a header button
| Param | Required | Description |
|---|---|---|
label |
✓ | Button text |
permission |
✓ | View permission |
url or event |
✓ | Open a URL or dispatch an event |
params |
— | Event params (with event only) |
order |
— | Order |
variant |
— | primary/secondary/danger/… |
icon |
— | Icon |
url takes precedence over event. The event is broadcast via Alpine
$dispatch and caught by Livewire #[On] or wire-elements
(openModal).
4) Zone
Main— the wide centre column.Sidebar— the side column.
The unified block chrome (<x-profile-block> / <x-profile-stat>)
The student profile is a unified block dashboard: every card renders inside
platform-owned chrome that shows its source (your app) with its accent
colour, a sync-status pill, last-synced time, and an "open in app" link. To
make your card blend into the board, wrap your panel content in
<x-profile-block> (instead of a bare <x-card>):
{{-- my-app::student-profile.attendance-summary --}}
<x-profile-block
:title="__('my-app::profile.attendance')"
:source="\App\Support\StudentProfile\BlockSource::resolve('my-app')"
status="live" {{-- live | syncing | offline | error --}}
:last-synced-at="$syncedAt"
:open-in-app-href="route('store.show', 'my-app')"
:open-in-app-label="__('my-app::profile.open')">
{{-- your content: chart / table / list — tenant data via App\Services\PartnerApi\* only --}}
<x-slot:actions>…</x-slot:actions> {{-- optional: ⋮ menu --}}
</x-profile-block>
BlockSource::resolve('<module-slug>')gives you a stable source identity (name / accent / icon) — don't invent a colour per card. (Or passsource-label/accent/icondirectly.)- Status is dynamic: pass
statusandlast-synced-atat render time from your real sync state.status="offline"shows an offline state — use<x-slot:offline>for the fallback (link-to-connect) content. - Stat tiles (
#[StudentStat]) use<x-profile-stat>(:label+:value+:source) so they appear in the stats row attributed to your app. - The platform owns the chrome; you supply content + status only. The "data sources" bar and the source filter are derived automatically from your registered blocks (no extra config).
How it shows in the portal
The partner portal does not run your code, so its Student Profile
tab is read-only: it lists the contributions okta-web discovered after
a sandbox install, and the published manifest's studentProfile block is
derived from them. You write the code — everything else is automatic.
Your component receives
studentHashidonly:<livewire:my-app::student-profile-summary :studentHashid="$studentHashid" />.