A web application for nocturne-image and the Spotify Car Thing.
How To Use • Spotify Developer Setup • Local Development Setup • Credits • Related • License
- First, follow the steps in Spotify Developer Setup
- Follow the steps for your operating system in the image's repo
- Once running on your Car Thing, press "Use Custom Credentials" and enter your Spotify Client ID and Client Secret using VNC Viewer
- Your credentials will be encrypted and stored securely
- Authorize with Spotify
- Start using the app
It is not recommended to use default credentials at this point in time. Unless you know what you're doing, please use custom credentials
- First, follow the steps in Spotify Developer Setup
- Follow the steps for your operating system in the image's repo.
- Once running on your Car Thing, hold the back button (under the knob) until the "Use Default Credentials (Beta)" button appears
- Press the "Use Default Credentials (Beta)" butto
- Authorize with Spotify
- Start using the app
- Hold one of the top hardware preset buttons while on a playlist page to map it to the button
- Press the mapped buttons to quickly play playlists
- Hold the right-most top hardware button to access settings
- Triple-press the right-most top hardware button to access brightness control
- Press the hardware button underneath of the knob to go back in the application
-
Go to the Spotify Developer Dashboard
-
Log in with your Spotify account (create one if needed)
-
Click "Create App"
- App name: Choose a name (e.g., "Nocturne")
- App description: Brief description of your app
- Redirect URI: Add
https://nocturne.brandons.place
for non-development usage, orhttps://your.local.ip.address:port
for development usage - Select "Web API" as the API you will be using
-
After creating the app, you'll see your:
- Client ID (shown on the dashboard)
- Client Secret (click "Show Client Secret")
-
Required Scopes: The app needs these Spotify scopes:
user-read-recently-played user-read-private user-top-read user-read-playback-state user-modify-playback-state user-read-currently-playing user-library-read user-library-modify playlist-read-private playlist-read-collaborative playlist-modify-public playlist-modify-private
- Clone the repository:
git clone https://github.com/yourusername/nocturne-ui.git
cd nocturne
- Install dependencies:
npm install
# or
yarn install
# or
bun install
- Copy the environment example file:
cp .env.example .env.local
- Spotify authentication requires HTTPS, follow these steps to set up HTTPS locally for development:
# macOS
brew install mkcert
# Windows (using chocolatey)
choco install mkcert
# Linux
apt install mkcert
# Create and install local CA
mkcert -install
# Generate certificates for localhost and your local IP
mkcert localhost 127.0.0.1 ::1 your.local.ip.address
# Rename the generated certificates
mv localhost+3.pem cert.crt
mv localhost+3-key.pem cert.key
# Copy CA certificates (locations vary by OS)
# macOS
cp "$(mkcert -CAROOT)/rootCA.pem" ca.crt
cp "$(mkcert -CAROOT)/rootCA-key.pem" ca.key
# Windows
copy "%LOCALAPPDATA%\mkcert\rootCA.pem" ca.crt
copy "%LOCALAPPDATA%\mkcert\rootCA-key.pem" ca.key
# Linux
cp "$(mkcert -CAROOT)/rootCA.pem" ca.crt
cp "$(mkcert -CAROOT)/rootCA-key.pem" ca.key
# Add to .gitignore
*.crt
*.key
*.pem
Create custom server file:
const { createServer } = require('https');
const { parse } = require('url');
const next = require('next');
const fs = require('fs');
const dev = process.env.NODE_ENV !== 'production';
const app = next({ dev });
const handle = app.getRequestHandler();
const httpsOptions = {
key: fs.readFileSync('./cert.key'),
cert: fs.readFileSync('./cert.crt')
};
app.prepare().then(() => {
createServer(httpsOptions, (req, res) => {
const parsedUrl = parse(req.url, true);
handle(req, res, parsedUrl);
}).listen(3000, (err) => {
if (err) throw err;
console.log('> Ready on https://localhost:3000');
console.log('> Also available on https://your.local.ip.address:3000');
});
});
- Set up your Supabase project:
- Create a new project at Supabase
- Create a new table named
spotify_credentials
with the following schema:
create table spotify_credentials (
id uuid default uuid_generate_v4() primary key,
temp_id text not null,
client_id text not null,
encrypted_client_secret text not null,
refresh_token text,
created_at timestamp with time zone default timezone('utc'::text, now()) not null,
expires_at timestamp with time zone,
last_used timestamp with time zone,
first_used_at timestamp with time zone,
token_refresh_count integer default 0,
user_agent text
);
-- Add policies for Row Level Security (RLS)
alter table spotify_credentials enable row level security;
-- Cleanup expired records
create policy "Cleanup expired records"
on spotify_credentials for all
using (expires_at < current_timestamp or refresh_token = current_setting('app.current_refresh_token'::text, true));
-- Cleanup unused temp IDs
create policy "cleanup_unused_temp_ids"
on spotify_credentials for all
using (created_at < current_timestamp - interval '1 hour' and refresh_token is null);
-- Enforce expiration
create policy "Enforce expiration"
on spotify_credentials for all
using (expires_at is null or expires_at > current_timestamp);
-- Update restrictions
create policy "Restricted updates"
on spotify_credentials for update
using (temp_id is not null)
with check (
client_id = (select client_id from spotify_credentials where id = id)
and encrypted_client_secret = (select encrypted_client_secret from spotify_credentials where id = id)
and temp_id = (select temp_id from spotify_credentials where id = id)
and created_at = (select created_at from spotify_credentials where id = id)
);
-- Insert validation
create policy "Validated inserts"
on spotify_credentials for insert
with check (
temp_id is not null
and client_id is not null
and encrypted_client_secret is not null
and length(temp_id) >= 7
and created_at <= current_timestamp
and (expires_at is null or expires_at > current_timestamp)
and not exists (
select 1 from spotify_credentials sc
where sc.temp_id = spotify_credentials.temp_id
)
);
- Generate encryption keys:
# Generate 32-byte key for AES-256
openssl rand -hex 32
# Generate 16-byte IV
openssl rand -hex 16
- Update your
.env.local
with the required values:
# Supabase
NEXT_PUBLIC_SUPABASE_URL=your_supabase_url
SUPABASE_ANON_KEY=your_anon_key
# Spotify
NEXT_PUBLIC_SPOTIFY_CLIENT_ID=your_client_id
SPOTIFY_CLIENT_SECRET=your_client_secret
NEXT_PUBLIC_REDIRECT_URI=https://your.local.ip.address:port
# Encryption (from step 5)
ENCRYPTION_KEY=your_32_byte_hex_key
ENCRYPTION_IV=your_16_byte_hex_key
- Start the development server:
npm run dev
# or
yarn dev
# or
bun dev
- Install Wrangler CLI:
npm install -g wrangler
- Login to Cloudflare:
wrangler login
- Create a KV namespace for encryption keys:
cd workers/key-rotation
npx wrangler kv:namespace create ENCRYPTION_KEYS
- Update the KV namespace ID in your root
wrangler.toml
:
name = "nocturne"
compatibility_date = "2024-09-23"
compatibility_flags = ["nodejs_compat"]
pages_build_output_dir = ".vercel/output/static"
kv_namespaces = [
{ binding = "ENCRYPTION_KEYS", id = "your_namespace_id_here" }
]
[vars]
NEXT_PUBLIC_REDIRECT_URI = "your_production_url"
NEXT_PUBLIC_SUPABASE_URL = "your_supabase_url"
NEXT_PUBLIC_SPOTIFY_CLIENT_ID = "your_client_id"
- Set up environment secrets:
# For the main app
npx wrangler secret put SUPABASE_ANON_KEY
npx wrangler secret put SPOTIFY_CLIENT_SECRET
npx wrangler secret put ENCRYPTION_KEY
npx wrangler secret put ENCRYPTION_IV
# For the key rotation worker
cd workers/key-rotation
npx wrangler secret put ENCRYPTION_KEY
npx wrangler secret put ENCRYPTION_IV
- Deploy the key rotation worker:
cd workers/key-rotation
npx wrangler deploy
The project includes automatic key rotation for encrypted credentials. The rotation worker runs every 7 days and:
- Generates new encryption keys
- Re-encrypts existing credentials
- Maintains backups during rotation
- Handles failures gracefully
The rotation schedule can be modified in workers/key-rotation/wrangler.toml
.
- Fork the repository
- Create a feature branch:
git checkout -b feature/new-feature
- Commit your changes:
git commit -m 'Add new feature'
- Push to the branch:
git push origin feature/new-feature
- Open a Pull Request
- Client secrets are encrypted using AES-256-CBC
- Keys are automatically rotated every 7 days
- Credentials are stored with expiration dates
- Unused credentials are automatically cleaned up
- All sensitive operations happen server-side
This software was made possible only through the following individuals and open source programs:
- Benjamin McGill, for giving me a Car Thing to develop with
- shadow, for OS development, testing, troubleshooting, and crazy good repo maintainence
- Dominic Frye, for OS development, debugging, testing, and marketing
- bbaovanc, for OS development, debugging, and testing
- bishopdynamics, for creating the original superbird-tool, and superbird-debian-kiosk
- Car Thing Hax Community's fork of superbird-tool, for their contributions on the original superbird-tool
nocturne-image - The Debian image that runs this web application
This project is licensed under the GPL-3.0 license.
We kindly ask that any modifications or distributions made outside of direct forks from this repository include attribution to the original project in the README, as we have worked hard on this. :)
brandons.place · GitHub @brandonsaldan · Twitter @brandonsaldan