The worst part of deploying a SPA is remembering which environment variables you forgot to set. The build succeeds, the site goes live, and the first API call returns 401 because you never pasted the backend URL. Most platforms make you type variables into a dashboard form after the deploy, which means you only discover missing config when things break in production.
PandaStack's pandastack.json config file flips that: you define which variables are required, add help text explaining where to get them, and the deploy screen shows a form that won't let you skip them. This guide builds a Solid.js app that calls an API, sets up the environment prompts, and shows how baked-in variables differ from runtime ones in a static build.
The Solid.js app with an API call
Create a new Solid.js project:
npx degit solidjs/templates/js solid-env-demo
cd solid-env-demo
npm installInstall an HTTP client:
npm install kyCreate a simple component that fetches data from an API:
// src/App.jsx
import { createSignal, onMount } from 'solid-js'
import ky from 'ky'
function App() {
const [data, setData] = createSignal(null)
const [error, setError] = createSignal(null)
const apiUrl = import.meta.env.VITE_API_URL || 'https://api.example.com'
const apiKey = import.meta.env.VITE_API_KEY
onMount(async () => {
try {
const response = await ky.get(`${apiUrl}/data`, {
headers: { 'X-API-Key': apiKey }
}).json()
setData(response)
} catch (err) {
setError(err.message)
}
})
return (
<div>
<h1>Solid.js Environment Demo</h1>
{error() && <p style="color: red;">Error: {error()}</p>}
{data() && <pre>{JSON.stringify(data(), null, 2)}</pre>}
<p>API URL: {apiUrl}</p>
</div>
)
}
export default AppVite (which Solid uses under the hood) bakes import.meta.env.VITE_* variables into the JavaScript bundle at build time. This is fundamentally different from server-side apps: the variable values are frozen when you run npm run build, not when the user loads the page. If you change VITE_API_KEY after deploy, nothing happens — you must rebuild the app to pick up the new value.
Test it locally with a .env file:
# .env
VITE_API_URL=https://jsonplaceholder.typicode.com
VITE_API_KEY=demo-keynpm run devThe app loads, calls the API (which ignores the key in this demo case), and shows the response. Now make it deployable.
Prevent missing variables with pandastack.json
Create pandastack.json in the repo root:
{
"type": "static",
"name": "solid-env-demo",
"buildCommand": "npm run build",
"outputDir": "dist",
"env": [
{
"key": "VITE_API_URL",
"description": "Backend API base URL (e.g., https://api.yourapp.com). Used for all /data fetches."
},
{
"key": "VITE_API_KEY",
"description": "API authentication key. Get this from your backend's settings page."
},
{
"key": "NODE_ENV",
"value": "production"
}
]
}The env array has two kinds of entries:
- 1Prompted variables (
key+description, novalue): The deploy screen shows a text field with the description as help text. You can't skip it. - 2Preset variables (
key+value): Set automatically, no prompt. Useful forNODE_ENV, feature flags, or non-sensitive defaults.
The prompted variables ensure you never deploy without setting the API URL and key. The preset ones save you from typing NODE_ENV=production every time.
Commit and push:
git add .
git commit -m "Add Solid app with env prompts"
git pushDeploy and see the prompts
Go to https://dashboard.pandastack.io/deploy?repo=yourname/solid-env-demo. The deploy screen:
- 1Reads
pandastack.jsonfrom the repo'smainbranch via GitHub's API - 2Parses the
envarray - 3Shows a form with two required fields:
- VITE_API_URL: Backend API base URL (e.g., https://api.yourapp.com)...
- VITE_API_KEY: API authentication key. Get this from...
- 1Pre-fills
NODE_ENV=production(read-only, no prompt)
Fill in the fields:
VITE_API_URL:https://your-backend.pandastack.ioVITE_API_KEY:your-actual-api-key
Click Deploy. PandaStack clones the repo, runs npm install, then runs npm run build with those variables injected. Vite reads them and bakes the values into dist/assets/index.abc123.js. The static output is uploaded to a CDN, and you get a live URL.
Open the site in a browser. The API call fires with the URL and key you provided. Inspect the JavaScript source — you'll see the values are hardcoded in the bundle:
const apiUrl = "https://your-backend.pandastack.io"
const apiKey = "your-actual-api-key"This is by design for SPAs: the backend URL and public API keys are part of the compiled JavaScript. If VITE_API_KEY is a secret that shouldn't be in the bundle, you've made an architectural mistake — SPAs can't keep secrets because the code runs in the browser. Move the sensitive logic to your backend and call it from the SPA.
Deploy from CI with the API
The prompts are great for manual deploys, but CI pipelines can't click buttons. Use the REST API to override the prompted variables with secrets from your CI system:
curl -X POST https://api.pandastack.io/v1/projects \
-H "Authorization: Bearer psk_live_your_token_here" \
-H "Content-Type: application/json" \
-d '{
"slug": "static",
"name": "solid-env-demo",
"repositoryName": "yourname/solid-env-demo",
"branch": "main",
"buildCommand": "npm run build",
"outputDir": "dist",
"env": [
{ "name": "VITE_API_URL", "value": "https://api.production.com" },
{ "name": "VITE_API_KEY", "value": "prod-key-from-secrets" }
]
}'The API payload's env array overrides the pandastack.json prompts. The NODE_ENV preset still applies (it merges in), but VITE_API_URL and VITE_API_KEY come from the API call instead of the form.
In GitHub Actions:
- name: Deploy to PandaStack
run: |
curl -X POST https://api.pandastack.io/v1/projects/${{ secrets.PROJECT_ID }}/deploy \
-H "Authorization: Bearer ${{ secrets.PANDASTACK_TOKEN }}" \
-H "Content-Type: application/json" \
-d '{
"branch": "main",
"env": [
{ "name": "VITE_API_URL", "value": "${{ secrets.API_URL }}" },
{ "name": "VITE_API_KEY", "value": "${{ secrets.API_KEY }}" }
]
}'The pandastack.json still documents what variables are required; CI just supplies the values from a different source.
Add a deploy button with the env parameter
The deploy button can hint at which variables to set, but it can't pre-fill values (that would leak secrets in the URL). Use the env query param to list the variable names:
[](https://dashboard.pandastack.io/deploy?repo=yourname/solid-env-demo&env=VITE_API_URL,VITE_API_KEY)The deploy screen still reads pandastack.json for the descriptions, but the env param highlights those two variables in the form. This is redundant if your config file already has the prompts, but it's useful for repos without a config file — the env param makes the form show fields for those names.
For a URL with more context:
[](https://dashboard.pandastack.io/deploy?repo=yourname/solid-env-demo&envDescription=https://github.com/yourname/solid-env-demo#environment-variables)The envDescription param adds a help link next to the env fields. Clicking it opens your README's environment section in a new tab.
What breaks and how to fix it
API calls work locally but fail in production: You set the variables in a local .env file but forgot to set them on PandaStack. The build succeeded with empty strings (Vite's default for missing vars), so apiUrl is "" and the fetch fails. The pandastack.json prompts prevent this — you can't deploy without filling them in.
Changing an env var doesn't update the site: You updated VITE_API_KEY in the dashboard settings but the site still uses the old key. Static builds bake variables at build time, so you must redeploy to pick up the new value. Click Redeploy in the dashboard, or call POST /v1/projects/{id}/deploy via the API.
Sensitive secrets appear in the JavaScript bundle: You put a database password or Stripe secret key in VITE_DB_PASSWORD. Any VITE_* variable is publicly readable in the compiled JavaScript. If it's a secret, it belongs on your backend, not in the SPA. Call an API route that uses the secret server-side.
Build fails with "VITE_API_URL is not defined": Vite only reads variables that start with VITE_. If you named it API_URL, Vite ignores it. Rename to VITE_API_URL or configure envPrefix in vite.config.js.
Comparing static and container env vars
Static sites (Solid, React, Vue) bake env vars at build time. Container apps (Express, Fastify, Django) read them at runtime. This has consequences:
| Aspect | Static (Solid.js) | Container (Express) |
|---|---|---|
| When vars are read | Build time | Startup time |
| How to update | Rebuild and redeploy | Just redeploy |
| Secrets safe? | No — in browser JS | Yes — server-side only |
| Example use | API URLs, feature flags | Database passwords, API keys |
For a full-stack app, you'd deploy the Solid frontend as a static site with VITE_API_URL, and the Express backend as a container with DATABASE_URL and real secrets. The frontend calls the backend; the backend holds the keys.
Next steps
You've deployed a Solid.js app with environment variable prompts that prevent missing config. The same pattern works for React (Create React App, Vite), Vue (Vite), Svelte (SvelteKit in static mode), or any framework that bakes env vars at build time.
For production SPAs, consider:
- A separate container app for the backend API (so secrets stay server-side)
- Feature flags via
VITE_ENABLE_ANALYTICS=truefor conditional code paths - Per-environment builds (staging vs production) with different
VITE_API_URLvalues
The pandastack.json config file ensures every deploy has the right variables set before the build even starts.
References
- [Solid.js documentation](https://www.solidjs.com/docs/latest)
- [Vite environment variables](https://vitejs.dev/guide/env-and-mode.html)
- [PandaStack environment guide](https://docs.pandastack.io/projects/env)
- [PandaStack deploy button](https://docs.pandastack.io/projects/deploy-button)