PHP SDK
ghayma/sdk is the PHP client for the Ghayma platform. It is one package with
two entry classes, one over each published contract:
Ghayma— the admin client, authenticated with a project API key (gsk_…) againstapi.ghayma.tech. Sub-clients->auth,->storageand->databasesadminister an auth app’s end users, read and write storage objects, and hand you database connection details. Server-side only.GhaymaAuth— the end-user auth client for one app, keyed by an app slug and an optional server key (ghs_…) againstauth.ghayma.tech. It registers, signs in and manages an app’s end users.
The admin client holds a secret and belongs in server code only. GhaymaAuth
runs the same end-user auth endpoints as the Client SDK
and ghayma_auth, for a PHP server rather than a browser or a
Flutter app.
The package is ghayma/sdk on Packagist;
the source lives in Ghayma-PHP-SDK .
Installation
composer require ghayma/sdkRequires PHP 8.2+. The SDK talks HTTP through the PSR-18 / PSR-17 interfaces and never hard-depends on a client — it discovers any installed implementation via php-http/discovery . If your app does not already ship one, add Guzzle and nyholm:
composer require guzzlehttp/guzzle nyholm/psr7You can also inject your own client and factories — see Bring your own HTTP client.
Admin client
Construct Ghayma with a project API key from Project → Settings → API
keys. The three sub-clients return typed, immutable DTOs
(Ghayma\Sdk\Model\*).
use Ghayma\Sdk\Ghayma;
$ghayma = new Ghayma('gsk_your_project_key');Auth-app users
Page through an app’s end users, block or restore sign-in, and write the roles embedded in their JWT:
$page = $ghayma->auth->listUsers('auth-app-id', page: 1, limit: 20); // UserPage
foreach ($page->users as $user) {
printf("%s via %s%s\n", $user->email, $user->provider->value, $user->disabled ? ' (disabled)' : '');
}
$stats = $ghayma->auth->stats('auth-app-id'); // AuthStats
$one = $ghayma->auth->getUser('auth-app-id', 'user-id'); // AuthUser
$ghayma->auth->disableUser('auth-app-id', 'user-id');
$ghayma->auth->enableUser('auth-app-id', 'user-id');
$ghayma->auth->reset2fa('auth-app-id', 'user-id');
// The new value reaches the user's access token at their next refresh.
$ghayma->auth->setAppMetadata('auth-app-id', 'user-id', ['roles' => ['admin']]);listApps(), getApp(), deleteUser() and resetLink() round out the
surface. ->auth here administers users — it does not sign them in; that is
GhaymaAuth below.
Storage
$ghayma->storage->uploadObject('bucket-id', 'images/photo.jpg', $bytes, 'image/jpeg');
$file = $ghayma->storage->downloadObject('bucket-id', 'images/photo.jpg'); // ObjectContent
$link = $ghayma->storage->presignDownload('bucket-id', 'images/photo.jpg'); // PresignedUrl — $link->url
$list = $ghayma->storage->listObjects('bucket-id', 'images/'); // ObjectListpresignUpload(), objectInfo(), deleteObject(), deleteBatch(),
deletePrefix() and credentials() (a bucket’s raw S3 keys) are also on
->storage.
Databases
$creds = $ghayma->databases->credentials('db-id'); // DatabaseCredentials
// $creds->host, $creds->port, $creds->username, $creds->password, $creds->internalUrl
$metrics = $ghayma->databases->metrics('db-id'); // DatabaseMetricslist() and get() return the databases themselves. Creating, resizing and
deleting infrastructure is not in the SDK — those live in the
console and the ghayma CLI.
End-user auth client
GhaymaAuth signs an app’s end users in. Construct it with the app slug and,
on a server, a server key:
use Ghayma\Sdk\GhaymaAuth;
use Ghayma\Sdk\Model\LoginSuccess;
use Ghayma\Sdk\Model\TwoFaRequired;
use Ghayma\Sdk\Model\TwoFaEnrollmentRequired;
$auth = new GhaymaAuth(appSlug: 'my-app', serverKey: getenv('GHAYMA_AUTH_SERVER_KEY') ?: null);
$result = $auth->login('[email protected]', $password, $clientIp);
if ($result instanceof LoginSuccess) {
$session = $result->session; // store $session->accessToken / ->refreshToken yourself
} elseif ($result instanceof TwoFaRequired) {
// Enrolled user: a TOTP code, or one of their recovery codes.
$session = $auth->verify2fa($result->challengeToken, $code);
} elseif ($result instanceof TwoFaEnrollmentRequired) {
// Enforced policy, user not enrolled yet: enrol, show the QR, confirm.
$enrollment = $auth->enrollTotp($result->enrollToken); // render $enrollment->otpauthUri as a QR
$confirmation = $auth->confirmTotp($code, $result->enrollToken);
// $confirmation->recoveryCodes is shown once; $confirmation->session is the signed-in session.
}login() returns a LoginResult (LoginSuccess | TwoFaRequired |
TwoFaEnrollmentRequired) — match it with instanceof. register() answers
the same way with a RegisterResult:
use Ghayma\Sdk\Model\RegisterSuccess;
use Ghayma\Sdk\Model\VerificationRequired;
$result = $auth->register('[email protected]', $password, name: 'Jane Doe', clientIp: $clientIp);
if ($result instanceof RegisterSuccess) {
$session = $result->session; // tokens issued immediately
} elseif ($result instanceof VerificationRequired) {
// Account created, tokens withheld until the emailed link is opened.
// $result->message, $result->user
}Once you hold a session, getUser(), updateUser(), changePassword(),
changeEmail(), refresh(), logout() and deleteAccount() act on it. The
password-recovery pair forgotPassword() / resetPassword() (with
verifyResetToken() to pre-check a link) and resendVerification() are here
too.
Stateless by design
GhaymaAuth stores no session. A PHP request is stateless, so every
user-scoped method takes the caller-held $accessToken (or $refreshToken)
that you stored — there is no in-client token store and no auto-refresh timer.
This is the point of contrast with the browser Client SDK
and the Dart client, which hold the session and refresh it for
you.
// A later request, with the tokens you persisted:
$user = $auth->getUser($accessToken); // User
$pair = $auth->refresh($refreshToken); // TokenPair — rotate before expiry
$auth->logout($refreshToken); // revoke one refresh tokenStore $session->accessToken and $session->refreshToken in your own session
store or cookie, and pass them back on the next request.
Server key and client IP
When you construct GhaymaAuth with a server key, pass the real end-user IP so
the auth service applies its rate limits to that user rather than to your
server. The key and the IP travel together or not at all — a forwarded IP
is only honoured from a caller that proves itself with the key — and the IP is
validated as a literal before it is sent:
$auth = new GhaymaAuth(appSlug: 'my-app', serverKey: 'ghs_…');
$auth->login($email, $password, $request->ip()); // sends X-Ghayma-Server-Key + X-Ghayma-Client-IPclientIp is accepted on the rate-limited entry points: register, login,
forgotPassword, exchangeCode and signInWithIdToken.
Never expose a server key to a browser — build GhaymaAuth in server code
only. On Ghayma the key is injected into every site of the project; see
where the key comes from for the
variable names and rotation.
OAuth
Build a provider sign-in URL for your login page, then complete the sign-in server-side:
$url = $auth->googleAuthUrl('https://app.example.com/auth/callback');
// PKCE (mobile / SPA): pass the code_challenge; the callback returns a one-time code.
$url = $auth->googleAuthUrl('https://app.example.com/auth/callback', $codeChallenge);
$session = $auth->exchangeCode($code, $codeVerifier, $clientIp); // PKCE one-time code
$session = $auth->signInWithIdToken($googleIdToken, $nonce, $clientIp); // native ID tokengithubAuthUrl() works the same way. The redirect_uri must match one of the
app’s Allowed Origins, and native client IDs must be registered under
Native client IDs, both in the console. Full walkthrough:
OAuth on Mobile.
Laravel
No facade or companion package is required — bind the clients in a service provider:
use Ghayma\Sdk\Ghayma;
use Ghayma\Sdk\GhaymaAuth;
public function register(): void
{
$this->app->singleton(Ghayma::class, fn () => new Ghayma(config('services.ghayma.api_key')));
$this->app->singleton(GhaymaAuth::class, fn () => new GhaymaAuth(
appSlug: config('services.ghayma.app_slug'),
serverKey: config('services.ghayma.server_key'),
));
}Type-hint Ghayma or GhaymaAuth anywhere the container resolves. Laravel’s
Guzzle satisfies the PSR-18/17 discovery. Forward the caller’s IP with
$request->ip().
Bring your own HTTP client
Both constructors accept an optional PSR-18 client and PSR-17 request/stream factories; when omitted, they are discovered:
$ghayma = new Ghayma('gsk_…', 'https://api.ghayma.tech', $psr18Client, $requestFactory, $streamFactory);
$auth = new GhaymaAuth('my-app', 'https://auth.ghayma.tech', 'ghs_…', $psr18Client, $requestFactory, $streamFactory);Errors
Every failure is a Ghayma\Sdk\Exception\GhaymaException carrying status,
errorCode and — on a 429 — retryAfter. Catch the base type broadly, or a
subclass to branch:
| Exception | Raised when |
|---|---|
UnauthorizedException | 401 — missing, malformed or expired credentials |
ForbiddenException | 403 — valid credentials, not entitled to the route or action |
NotFoundException | 404 — no such resource for these credentials |
RateLimitedException | 429 — too many requests; retryAfter holds the cooldown in seconds when known |
InvalidGrantException | an expired or already-spent token/code (code: invalid_grant) |
InvalidTokenException | a token that failed verification (code: invalid_token) |
NetworkException | a transport failure before any HTTP status (status 0) |
use Ghayma\Sdk\Exception\RateLimitedException;
use Ghayma\Sdk\Exception\GhaymaException;
try {
$auth->login($email, $password, $clientIp);
} catch (RateLimitedException $e) {
// back off for $e->retryAfter seconds
} catch (GhaymaException $e) {
// $e->status, $e->errorCode, $e->getMessage()
}Contracts and conformance
Every wire shape comes from the two published OpenAPI contracts, vendored in the repository: the Runtime API (the project-key surface) and the Auth Service API (the end-user auth surface). Both are refreshed from the documents the services publish, and the SDK is run against a Prism mock of each — so an off-contract request fails the run rather than passing silently.