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() (the site’s own S3 key for a bucket) are
also on ->storage.
Databases
$creds = $ghayma->databases->credentials('db-id'); // DatabaseCredentials
// $creds->host, $creds->port, $creds->username, $creds->password, $creds->internalUrl,
// $creds->level, $creds->credential
$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.
Both credentials() calls return the site’s own connection credential, and
only to a site’s managed GHAYMA_API_KEY: a database’s own login and a
bucket’s own key are never handed out. level is the connection’s level and
credential is always connection; both need ghayma/sdk 0.4.0 or later. A
service the site isn’t connected to, or a key you created yourself, is refused
with 403; a connection that has no credential of its own yet with 409
(no_own_credential) — the platform gives it one as soon as the service can,
so retry later; and an account token with 410 (shared_credentials_retired).
From your laptop, use ghayma connect --local for a database and
ghayma env pull for a bucket. See Managed keys.
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.
The stored tokens stop working when the user’s sessions
end — a password reset or
change (changePassword() included, which ends the session you stored too), a
confirmed email change, a Google or GitHub takeover, a stolen refresh token, or
the user being disabled. refresh() then throws an UnauthorizedException,
and so do the calls that take the access token (a ForbiddenException while
the user is disabled): drop the stored tokens and sign the user in again.
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->header('X-Real-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 connected to the auth
app; 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 be on one of the
project’s domains, on localhost, or on an origin listed in the app’s Allowed
Origins (a custom scheme always needs its exact entry), and native client IDs
must be registered under Native client IDs, both in the console. Full walkthrough:
OAuth on Mobile.
signInWithIdToken() refuses a Google account whose address Google has not
verified: a 403 ForbiddenException with errorCode email_not_verified.
Second factor on OAuth sign-in
When the app’s 2FA policy applies to the user, exchangeCode() and
signInWithIdToken() create no session and throw a
TwoFactorRequiredException. Its result is the pending step login()
returns, finished the same way:
use Ghayma\Sdk\Exception\TwoFactorRequiredException;
use Ghayma\Sdk\Model\TwoFaEnrollmentRequired;
use Ghayma\Sdk\Model\TwoFaRequired;
try {
$session = $auth->exchangeCode($code, $codeVerifier, $clientIp);
} catch (TwoFactorRequiredException $e) {
if ($e->result instanceof TwoFaRequired) {
$session = $auth->verify2fa($e->result->challengeToken, $totpCode);
} elseif ($e->result instanceof TwoFaEnrollmentRequired) {
$enrollment = $auth->enrollTotp($e->result->enrollToken); // render $enrollment->otpauthUri as a QR
$confirmation = $auth->confirmTotp($totpCode, $e->result->enrollToken); // ->session, ->recoveryCodes
}
}$e->errorCode is two_fa_required or two_fa_enrollment_required, and
$e->status is 200. The exception extends GhaymaException, so a broad
catch (GhaymaException $e) still stops the sign-in; catch
TwoFactorRequiredException first to offer the second factor.
Update to ghayma/sdk 0.2.0 before you turn 2FA on. 0.1.0 never receives
tokens for a user the policy applies to, but exchangeCode() and
signInWithIdToken() fail with a PHP ValueError on the pending step. See
Minimum versions for 2FA.
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 visitor’s IP from the
X-Real-IP header ($request->header('X-Real-IP') in Laravel,
$_SERVER['HTTP_X_REAL_IP'] in plain PHP).
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) |
TwoFactorRequiredException | an OAuth sign-in stopped at the second factor (status 200); result holds the pending step — see Second factor on OAuth sign-in |
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.