tomatophp/laravel-agora
Integrate Agora real-time voice and video into your Laravel app with ready-made helpers for channel and token generation, configuration management, and quick setup so you can add live audio and video features without writing boilerplate.
This package does one thing: mint Agora RTC tokens server-side. Joining a channel, rendering video, muting tracks — that is entirely the Agora client SDK's job. Internalize that boundary before you install anything, because it determines what you will and won't find in src/.
Install and publish:
composer require tomatophp/laravel-agora
php artisan vendor:publish --tag=agora-config # verify the actual tag in README / src
Then set credentials from the Agora console (Project Management → Configure):
AGORA_APP_ID=your_app_id
AGORA_APP_CERTIFICATE=your_app_certificate
The App Certificate is a signing secret. It stays on the server — never in a client bundle, never in a log line, never in a committed .env.example.
Where to look first, in order:
config/agora.php after publishing — exact key names for App ID, certificate, and any TTL/expiry defaults, plus whether values are read via env() or config().src/ end to end. The whole package is a few hundred lines. You are adopting source code, not consuming a documented API; the source is the spec. Note the exact method signature for token generation before writing any integration code.Your first use case is this loop:
POST /rooms/{room}/token on your Laravel app.{ appId, channel, uid, token, expiresIn } and passes it to AgoraRTC.joinChannel(token, channel, uid).onTokenPrivilegeWillExpire fires, it calls the same endpoint for a fresh token.Get steps 1–3 working against a real join on one platform (Web is the fastest loop) before anything else. A token that "generates successfully" proves nothing — only a successful join does.
Highest-leverage ten minutes you'll spend. Keep the vendor name out of controllers and domain code.
// app/Contracts/AgoraTokenIssuer.php
interface AgoraTokenIssuer
{
public function issue(string $channel, string $uid, string $role, int $ttlSeconds): string;
}
// app/Services/Agora/AgoraTokenIssuerAdapter.php
final class AgoraTokenIssuerAdapter implements AgoraTokenIssuer
{
public function issue(string $channel, string $uid, string $role, int $ttlSeconds): string
{
// Match the package's real signature — read src/ first.
return Agora::generateToken($channel, $uid, $role, $ttlSeconds);
}
}
Bind it in a service provider. Now the package appears in exactly one file. When you fork the class, upgrade, or swap to Agora's official PHP library, the diff is one class and your tests don't move.
The API should accept intent ("join room 42 of team 7"), never a raw channel name or role string. Client-supplied channel names are channel squatting waiting to happen.
final class IssueRoomToken
{
public function __construct(private AgoraTokenIssuer $issuer) {}
public function __invoke(User $user, Team $team, string $roomId): array
{
Gate::authorize('joinRoom', [$team, $roomId]);
$channel = "t{$team->id}-room-{$roomId}"; // namespaced
$uid = (string) $user->id; // derived, not accepted
$role = $user->can('publish', $team) ? 'publisher' : 'subscriber';
$ttl = $team->plan->agoraTokenTtlSeconds();
return [
'appId' => config('agora.app_id'),
'channel' => $channel,
'uid' => $uid,
'token' => $this->issuer->issue($channel, $uid, $role, $ttl),
'expiresIn' => $ttl,
];
}
}
The uid decision is easy to get wrong and painful to debug: Agora treats integer and string UIDs as distinct namespaces. Pick one — stringified internal user IDs or an opaque per-session ID — and be consistent everywhere, or you get ghost duplicate participants.
Token TTL is a monetizable lever: 40-minute free sessions, all-day paid webinars, audience-only versus publisher seats. Keep it in a plan/tier model, not a hardcoded constant at the call site, so product can experiment without a deploy.
Prefer a short TTL plus client-driven renewal over a long-lived token. A 30–60 minute token refreshed via onTokenPrivilegeWillExpire is safer and more flexible than a 24-hour token you can't revoke.
Route::middleware(['auth:sanctum', 'throttle:agora-tokens'])
->post('/teams/{team}/rooms/{room}/token', IssueRoomToken::class);
The package ships no test that validates a minted token against Agora's own validator. Write two:
Carbon::setTestNow() — same inputs and TTL must produce the same token. Catches expiry-unit mistakes cold.For admin panels and internal tools, the Blade/Filament-friendly setup gets you a join screen in an afternoon. Keep the token endpoint a normal authenticated route and pass appId, channel, uid, token into the view — don't let the view call the service directly.
A missing or rotated App Certificate is a 100% join failure across all users, and it presents as a client bug. Everyone just "can't connect." Keep a synthetic join check running on a schedule in staging, and write the rotation runbook before you need it.
Verify the release history yourself. Check Packagist and GitHub tags rather than trusting metadata. You're effectively on a single-maintainer package with zero dependents. Pin the commit in composer.json and name an internal owner — or fork the ~150 lines of real logic into app/Support/Agora under MIT. For a security-critical signing path, that's a defensible default.
config:cache breaks raw env() calls. If the service reads env('AGORA_APP_ID') at runtime instead of through config(), tokens work locally and fail in production. If you spot it in the source, fix it in your fork: surface the keys in config/agora.php and read via config(). Run php artisan config:cache in CI.
Token version drift. AccessToken v1 vs v2 is not cosmetic — newer Agora project configurations reject v1 outright. Check which builder the package wraps. If the builder is vendored rather than a Composer dependency on Agora's official library, you've inherited a snapshot and own the schema-watch.
Clock skew yields "token not yet valid" / "token expired" at join. Your server clock is the source of truth. Run NTP; don't backdate tokens to "give them more time."
Never accept a client channel name verbatim. Namespace by tenant/room and enforce membership server-side, or you get cross-tenant token reuse and channel squatting with no after-the-fact detection.
Don't log tokens — not request logs, not audit trails, not exception payloads. Log the issuance event and correlate by request ID.
Renewal is where production bugs live. onTokenPrivilegeWillExpire is the path nobody tests until a long session drops. Exercise it in staging with a deliberately short TTL (e.g. 120s) so you hit renewal inside one manual session.
Namespace collisions. agora is a generic config key and facade alias. If you later add Agora Chat, RTM, Cloud Recording, or webhooks, check aliases and config namespaces first — this package covers tokens only. Recording, Media Push, Chat/RTM, REST wrappers, and webhook handling are all absent and must be built separately.
When you outgrow the wrapper (RTM/Chat tokens, per-tenant App IDs for resale, wildcard channels, fine-grained privilege expiry), check whether the underlying builder is exposed. If not, implement your AgoraTokenIssuer against Agora's official PHP library — the interface you wrote on day one makes that swap a single file.
How can I help you explore Laravel packages today?