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() (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'); // 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.

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.

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

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

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)
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.