Shipyard authentication is browser-driven. Your frontend redirects the user to GET /api/auth/github, which sends the browser to GitHub’s authorization page. After the user approves access, GitHub redirects back to GET /api/auth/github/callback with a one-time code. The server exchanges that code for a GitHub access token, upserts a user record, signs a JWT, and redirects the browser to your frontend with the token in the query string. Neither endpoint requires an existing JWT — they are the mechanism that produces one.
GET /api/auth/github
Redirects the browser to GitHub’s OAuth authorization page. No request body or authentication is needed.
No auth required.
When the user visits this URL, the server responds with 302 and sets the Location header to:
https://github.com/login/oauth/authorize?client_id=<CLIENT_ID>&scope=read:user,repo,read:org&prompt=consent
The prompt=consent parameter forces the GitHub authorization screen to appear even if the user previously authorized the app, so they always see which scopes are being requested.
Requested scopes
| Scope | Purpose |
|---|
read:user | Read the user’s GitHub profile (login, avatar, email) |
repo | Clone private repositories and register webhooks |
read:org | List organizations the user belongs to |
Curl example
Visiting this URL in a browser is the intended use. To inspect the redirect target without following it:
curl -I https://your-server/api/auth/github
HTTP/1.1 302 Found
Location: https://github.com/login/oauth/authorize?client_id=Iv1.abc123&scope=read:user,repo,read:org&prompt=consent
GET /api/auth/github/callback
Handles the OAuth callback from GitHub. GitHub appends a one-time code to this URL after the user authorizes the app. The server exchanges the code for an access token, fetches the user’s GitHub profile, upserts a user record in the database, signs a JWT, and redirects the browser to FRONTEND_URL/auth/callback with the session data in the query string.
No auth required.
Query parameters
The one-time authorization code provided by GitHub after the user grants access. GitHub appends this to your callback URL automatically — you do not construct or send it yourself.
Success redirect
On success, the server responds with 302 and redirects to:
<FRONTEND_URL>/auth/callback?token=<jwt>&username=<login>&avatar=<avatar_url>&email=<email>&createdAt=<timestamp>
The query parameters carry everything your frontend needs to initialize a session:
| Parameter | Description |
|---|
token | Signed JWT, valid for 1 hour. Pass this as Authorization: Bearer <token> on all subsequent API requests. |
username | The user’s GitHub login |
avatar | URL of the user’s GitHub avatar |
email | Primary verified email address. Shipyard falls back to /user/emails if the profile email is hidden. |
createdAt | Timestamp of when the user record was first created in Shipyard |
Error responses
| Status | Condition |
|---|
400 | The code query parameter is missing from the request |
500 | GitHub did not return an access token, or the user profile could not be fetched |
Handling the callback in your frontend
When the browser lands on your frontend’s /auth/callback route, parse the query parameters and store the token for subsequent API calls:
// On your callback page at /auth/callback
const params = new URLSearchParams(window.location.search);
const token = params.get("token");
const username = params.get("username");
const avatar = params.get("avatar");
const email = params.get("email");
const createdAt = params.get("createdAt");
// Store the token for subsequent API calls
localStorage.setItem("shipyard_token", token);
Then attach the token to every authenticated request:
const response = await fetch("/api/project/projects", {
headers: {
Authorization: `Bearer ${localStorage.getItem("shipyard_token")}`,
},
});
The JWT expires after 1 hour. When it expires, the user must go through the OAuth flow again — redirect them to /api/auth/github to re-authenticate.