Skip to Content

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_…) against api.ghayma.tech. Sub-clients ->auth, ->storage and ->databases administer 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_…) against auth.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/sdk

Requires 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/psr7

You 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/'); // ObjectList

presignUpload(), 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'); // DatabaseMetrics

list() 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 token

Store $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-IP

clientIp 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 token

githubAuthUrl() 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:

ExceptionRaised when
UnauthorizedException401 — missing, malformed or expired credentials
ForbiddenException403 — valid credentials, not entitled to the route or action
NotFoundException404 — no such resource for these credentials
RateLimitedException429 — too many requests; retryAfter holds the cooldown in seconds when known
InvalidGrantExceptionan expired or already-spent token/code (code: invalid_grant)
InvalidTokenExceptiona token that failed verification (code: invalid_token)
NetworkExceptiona transport failure before any HTTP status (status 0)
TwoFactorRequiredExceptionan 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.