How developers sign in to DeployYourApp, how CI authenticates, and how permissions are decided. Authentication is built on Better Auth with a PostgreSQL adapter.
Email and password
The default method. You register with an email address and a password.
| Property | Value |
|---|---|
| Hash function | scrypt |
| Parameters | N=16384, r=16, p=1, dkLen=64 |
| Salt | 16 random bytes per password |
| Stored form | hex(salt):hex(derivedKey) |
| Comparison | Constant-time |
| Minimum length | 8 characters |
Plaintext passwords are never stored and never logged.
Email verification is required. You cannot sign in until you follow the link in the verification email.
Sign in at /auth/login in the dashboard.
Signing in from the CLI
dya login
The CLI prints an authorization URL, waits for you to press ENTER before opening a browser, and then polls until you approve. Nothing listens on your machine, so this also works over SSH, inside containers, and when the browser runs on a different host.
Four things constrain that flow:
- The approval page shows which client is asking, the IP the request came from, and when — so a request you did not start is recognisable before you approve it.
- The poll token is a separate secret from the session id. The id appears in the URL you might
paste into chat or screen-share; the token travels only in the
X-Poll-Tokenheader, and is stored server-side as a SHA-256 hash. - Approval is single-use. Claiming is a conditional update, so two racing polls cannot both
mint a token — the loser gets
410. - A pending request expires after 10 minutes, and denying in the browser fails the waiting terminal immediately.
If your account has 2FA enabled, the browser enforces it before the request can be approved. The CLI never handles your password or authenticator secret — only the session token.
CLI 0.1.x used a localhost callback on ports 9876–9895, constrained to a loopback URL with
a one-time state nonce. The server still accepts that flow so installed copies keep working.
CLI session tokens are 32 random bytes, hex-encoded, and expire after 30 days. They are
recorded with the user agent DeployYourApp CLI and appear in your active sessions list.
For CI, use an API key instead:
dya login --api-key dya_...
Social sign-in
Three providers are supported, each enabled only when the platform operator has configured both its client ID and client secret:
- GitHub
- Microsoft (multi-tenant,
tenantId: common)
Social sign-in is available on every plan.
Two-factor authentication
Two-factor uses time-based one-time passwords.
| Property | Value |
|---|---|
| Algorithm | TOTP |
| Digits | 6 |
| Period | 30 seconds |
| Backup codes | Issued at enrollment |
Enabling it
- Go to Settings > Security in the dashboard.
- Click Enable 2FA and confirm with your password.
- Scan the QR code with an authenticator app.
- Save the backup codes shown on screen — they are displayed once.
- Enter the 6-digit code to confirm enrollment.
At sign-in you are prompted for a code after your password. You can answer with a backup code instead if you have lost your authenticator.
Organization two-factor policy
Owners and admins can enforce two-factor for the whole organization under Settings > Organization > Security Policy.
| Setting | Effect |
|---|---|
| Require two-factor authentication | Members without 2FA on their account are blocked from every organization resource with TWO_FACTOR_REQUIRED. API keys inherit the 2FA state of the user who owns them. You must have 2FA enabled yourself before you can turn this on. |
| Require a 2FA code for bundle uploads | Every bundle upload must carry a fresh authenticator code in the X-2FA-Code header. dya deploy prompts for it when the server answers TWO_FACTOR_CODE_REQUIRED. |
Upload codes are single-use. The server records the consumed time step in Redis for two periods,
so a code captured from a CI log cannot be replayed to push a second bundle. A replayed code is
rejected with TWO_FACTOR_CODE_REPLAYED.
Sessions
Dashboard and CLI sessions are database-backed cookie sessions, not JWTs. The cookie prefix is
dya.
| Property | Value |
|---|---|
| Dashboard session lifetime | 7 days |
| CLI session lifetime | 30 days |
| Refresh interval | 24 hours |
The API also accepts Authorization: Bearer <session token>, which is how the single-page
dashboard and the CLI authenticate.
Cross-origin requests are restricted to an explicit origin allow-list. There is no subdomain wildcard.
View and revoke sessions under Settings > Security > Active Sessions. Each entry shows the device or browser, IP address, and last-active time. Use Revoke for one session, or revoke all others to sign out every other device.
Suspending an account invalidates access immediately on both the session and API key paths — it does not wait for the session to expire.
API keys
API keys give programmatic access without an interactive login.
| Property | Value |
|---|---|
| Format | dya_ followed by 32 random bytes, hex-encoded |
| Storage | SHA-256 hex hash. The raw key is never stored |
| Shown in full | Once, at creation |
| Scope binding | One organization, fixed at creation |
| Expiry | Optional, set in days at creation |
A key is rejected if it is revoked, expired, or owned by a suspended user. Every accepted request updates the key's last-used timestamp.
A key created in one organization cannot be used against another, even one its creator belongs to.
Creating a key
- Go to Settings > API Keys.
- Click Create API Key.
- Name it, select scopes, and optionally set an expiry.
- Copy the key immediately.
Or via the API:
curl -X POST https://api.deployyour.app/api/orgs/$ORG_ID/api-keys \
-H "Content-Type: application/json" \
-H "X-API-Key: $ADMIN_KEY" \
-d '{"name":"ci-deploy","scopes":["app:read","channel:read","bundle:upload","deployment:create"],"expiresInDays":90}'
scopes defaults to ["*"] if omitted. Unknown scope strings are rejected with a validation
error.
Using a key
dya login --api-key dya_...
dya deploy
Or as a header on any API request:
X-API-Key: dya_...
Scopes
Scopes are permission strings — the same vocabulary the role model uses. A key is limited to the scopes assigned to it, and is additionally limited by the role of the user who created it.
| Scope | Grants |
|---|---|
* |
Every permission |
app:read app:create app:update app:delete |
App management |
channel:read channel:create channel:update channel:delete |
Channel management |
bundle:read bundle:upload bundle:delete |
Bundle management |
deployment:read deployment:create deployment:rollback |
Deployment control |
device:read |
View devices |
member:invite member:remove member:update-role |
Membership management |
settings:read settings:write |
Organization settings |
apikey:create apikey:revoke |
API key management |
billing:read billing:manage |
Billing |
audit:read |
Audit log |
build:read build:trigger build:cancel |
Cloud builds |
A typical CI deploy key needs only:
["app:read", "channel:read", "bundle:upload", "deployment:create"]
Handling keys well
- One key per pipeline or integration, so a single revocation is surgical.
- Assign the narrowest scope set that works.
- Set an expiry on anything temporary.
- Revoke immediately when a key is no longer needed.
- Never commit a key to version control.
Roles and permissions
An organization has four roles. The creator of an organization becomes its owner. An organization holds up to 100 members, and a user can create up to 5 organizations.
| Permission | Owner | Admin | Developer | Viewer |
|---|---|---|---|---|
app:read |
yes | yes | yes | yes |
app:create app:update app:delete |
yes | yes | — | — |
channel:read |
yes | yes | yes | yes |
channel:create channel:update channel:delete |
yes | yes | — | — |
bundle:read |
yes | yes | yes | yes |
bundle:upload |
yes | yes | yes | — |
bundle:delete |
yes | yes | — | — |
deployment:read |
yes | yes | yes | yes |
deployment:create |
yes | yes | yes | — |
deployment:rollback |
yes | yes | — | — |
device:read |
yes | yes | yes | yes |
member:invite member:remove member:update-role |
yes | yes | — | — |
settings:read |
yes | yes | yes | yes |
settings:write |
yes | yes | — | — |
apikey:create apikey:revoke |
yes | yes | — | — |
billing:read |
yes | yes | — | — |
billing:manage |
yes | — | — | — |
audit:read |
yes | yes | — | — |
build:read |
yes | yes | yes | yes |
build:trigger |
yes | yes | yes | — |
build:cancel |
yes | yes | — | — |
Owners hold the wildcard permission *. Cloud builds are currently disabled by the
CLOUD_BUILDS_ENABLED feature flag, so the build:* permissions have no effect yet.
Every organization-scoped request re-checks membership against the database. A stale role carried on the request object is never trusted.
Security notification emails
You receive an automated email whenever a sensitive change is made to your account.
| Event | Sent when |
|---|---|
| Password changed | Your password is changed from Settings > Profile |
| Password reset | A password-reset link is used to set a new password |
| 2FA enabled | Two-factor enrollment completes |
| 2FA disabled | Two-factor is turned off |
Each email includes the time, IP address, and device that made the change. These cannot be disabled. If one arrives that you do not recognise, reset your password and review your active sessions.
Account deletion
Deleting your account is self-serve, and confirmation is by email rather than by session alone. Requesting deletion sends a link that expires in 24 hours; clicking it performs the deletion.
The request is refused up front if your account is the sole owner of a live organization. Transfer ownership or delete the organization first.
Rate limits
Limits are the same on every plan and are keyed by IP address, authenticated or not. Rate limiting runs before authentication, so the identity behind a request is not yet known when the counter is chosen — and keying on an unverified credential from the request would let anyone mint a fresh bucket per attempt. One consequence is worth planning for: clients behind shared NAT, a corporate egress IP, or a CI runner share a bucket.
Redis backs the counters. The /health and /ready endpoints are exempt.
| Endpoint | Limit | Window |
|---|---|---|
| Global default, all API requests | 100 | 1 minute |
| Sensitive auth paths (sign-in, sign-up, password reset, verification email, 2FA verify/enable/disable), per IP | 20 | 15 minutes |
| Bundle upload | 30 | 1 hour |
Device update check POST /api/update |
60 | 1 minute |
Analytics ingestion POST /api/stats |
100 | 1 minute |
| Organization invitations | 20 | 1 hour |
Personal data export GET /api/me/export |
5 | 1 hour |
| Corporate shell access-code lookup | 10 | 1 hour |
| Notification unsubscribe | 30 | 1 hour |
Exceeding a limit returns 429:
{
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "Too many requests. Please try again later."
}
}
Recommendations
- Enable 2FA on every account, and turn on the organization-wide requirement once your team has.
- Turn on the 2FA-for-uploads policy if a compromised CI key would be unacceptable.
- Give CI the narrowest API key scopes that work, and set an expiry.
- Review active sessions periodically and revoke what you do not recognise.
- Watch the audit log for unexpected activity. It is available on the Scale plan and above.