> Solana Developer Platform docs, served as Markdown. The [docs index](https://platform-beta.solana.com/docs.md) lists every page and [llms.txt](https://platform-beta.solana.com/docs/ai/llms.txt) the key ones; [SKILL.md](https://platform-beta.solana.com/SKILL.md) has the rules for building on SDP.

# First Devnet Deployment

Run self-hosted SDP on devnet, sign in to the dashboard, create a local wallet, and verify API access.

Take a fresh self-hosted install from healthy containers to a first devnet workflow: dashboard sign-in, a local custody wallet, a wallet-scoped API key, and an authenticated API call.

Start after the [Quickstart](/docs/self-hosting/quickstart). This path runs on devnet with the local signer. Set up a provider's signing later, when you harden the deployment.

**warn**

The public devnet RPC endpoint is fine for smoke tests, but it can rate-limit you or miss data that balance-heavy wallet views need. Use a dedicated devnet endpoint from your RPC provider before you rely on this flow.

<Steps>

<Step>

## Confirm the stack is healthy

From the directory that holds `compose.yml` and `.env`:

```bash title="Terminal"
cd ~/sdp
docker compose up -d
docker compose ps
curl http://localhost:8787/health
```

The health check returns JSON with `"status": "ok"`. The dashboard is at `http://localhost:3000`, the API at `http://localhost:8787`, and these docs at `http://localhost:3001`.

</Step>

<Step>

## Configure Clerk for dashboard sign-in

In your Clerk development application, follow the [Clerk setup guide](https://github.com/solana-foundation/solana-developer-platform/blob/main/apps/sdp-api/docs/self-hosting/clerk-setup.md):

- Turn on **Organizations**.
- Customize the session token (**Sessions** → **Customize session token**) with the guide's claims. Without `org_id`, the API refuses every dashboard request.
- Copy the publishable key, secret key, and issuer for the configurator.
- Give the API a public HTTPS URL, because Clerk can't reach `localhost`. To test locally, reserve a free static domain at [dashboard.ngrok.com/domains](https://dashboard.ngrok.com/domains) and run `ngrok http --url=<your-domain> 8787`.
- Add a webhook endpoint at `https://<your-domain>/webhooks/clerk/link-orgs`, subscribed to the events the guide lists. Copy its signing secret for `CLERK_WEBHOOK_SECRET`.

The API starts without `CLERK_WEBHOOK_SECRET`, but this tutorial needs it. The webhook is how SDP learns the settings you add to the organization in Clerk, including the one that turns on the local signer.

</Step>

<Step>

## Generate the devnet `.env`

Open the [configurator](/docs/self-hosting/configurator), keep its defaults, and fill in:

- `SOLANA_RPC_DEFAULT_DEVNET_API_KEY_URL`: your devnet RPC endpoint.
- The Clerk publishable key, secret key, issuer, and webhook secret.

The signing step keeps the local signer and generates its `CUSTODY_PRIVATE_KEY` in your browser. SDP offers the local signer only while that key is set. Save the file as `~/sdp/.env`, then restart the stack:

```bash title="Terminal"
cd ~/sdp
docker compose up -d
```

</Step>

<Step>

## Sign in to the dashboard

Open `http://localhost:3000`, sign in through Clerk, and create an organization when prompted. SDP creates it with you as its admin, along with a sandbox project and a production project.

If requests fail after sign-in, read the failing request's `401` response. `Clerk token missing organization` means the session token lacks the `org_id` claim, and `Clerk token has no usable email claim` means its `email` claim is wrong: fix the session token customization.

If Clerk's webhook deliveries fail, check the API logs:

```bash title="Terminal"
docker compose logs -f sdp-api
```

`webhooks: refused a Clerk webhook with an invalid signature` means `CLERK_WEBHOOK_SECRET` doesn't match the endpoint's signing secret. Clerk's delivery log shows the same refusal as `Invalid webhook signature`.

</Step>

<Step>

## Turn on the local signer

The local signer needs a per-organization activation. In the Clerk dashboard, open your organization's private metadata (**Organizations** → your organization → **Metadata** → **Private**) and add:

```json
{ "sdp": { "providerOverrides": { "custody": { "local": true } } } }
```

The `organization.updated` webhook brings it into SDP. Until it does, choosing the local signer returns `403` with `Local requires manual activation for this organization.`

</Step>

<Step>

## Create a local custody wallet

In the dashboard, open **Wallets**, choose the local provider, and create a wallet named `Devnet local wallet`.

This step needs the dashboard. Setting up a custody provider takes `wallets.configure`, which no API key can hold, so an organization admin does it signed in. After that, a key with `wallets.manage` can create more wallets with `POST /v1/wallets`.

</Step>

<Step>

## Create a wallet-scoped API key

Open **API keys** and create a key in the sandbox project. Under **Permissions**, grant at least `wallets.read`. Under **Access**, choose **Selected wallets** and select `Devnet local wallet`: it's the only wallet the key can sign with.

Copy the full key when the dashboard shows it. SDP shows it only once.

</Step>

<Step>

## Verify the key can read wallets

Call your self-hosted API with the new key:

```bash title="Terminal"
export SDP_API_KEY="sf_..."

curl "http://localhost:8787/v1/wallets?view=summary" \
  -H "Authorization: Bearer $SDP_API_KEY"
```

The response includes `Devnet local wallet`. You now have a self-hosted devnet stack with dashboard sign-in, local custody signing, and an API key bound to the wallet it may sign with.

</Step>

</Steps>

## Next steps

- Use the [Environment reference](/docs/self-hosting/env-reference) to move from localhost defaults to real domains, an external Postgres, or an external Redis.
- Read [Upgrade and backup](/docs/self-hosting/upgrade-and-backup) before you keep data you care about in the stack.
- Use [External co-signers](/docs/self-hosting/external-co-signers) when a provider needs a signing process outside the default stack.
