diff --git a/.github/macos/entitlements.plist b/.github/macos/entitlements.plist
new file mode 100644
index 000000000..c53774e6a
--- /dev/null
+++ b/.github/macos/entitlements.plist
@@ -0,0 +1,18 @@
+
+
+
+
+ com.apple.security.cs.disable-library-validation
+
+ com.apple.security.network.client
+
+ com.apple.security.network.server
+
+ com.apple.security.files.user-selected.read-only
+
+ com.apple.security.files.user-selected.read-write
+
+ com.apple.security.files.downloads.read-write
+
+
+
diff --git a/.github/workflows/macos-signing.yml b/.github/workflows/macos-signing.yml
new file mode 100644
index 000000000..6fb371721
--- /dev/null
+++ b/.github/workflows/macos-signing.yml
@@ -0,0 +1,75 @@
+name: macOS signing
+
+on:
+ pull_request:
+ branches: [main]
+ paths:
+ - scripts/sign-macos.sh
+ - .github/macos/entitlements.plist
+ - .github/workflows/macos-signing.yml
+ push:
+ branches: [main]
+ paths:
+ - scripts/sign-macos.sh
+ - .github/macos/entitlements.plist
+ - .github/workflows/macos-signing.yml
+
+permissions:
+ contents: read
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ sign-macos:
+ runs-on: macos-latest
+ timeout-minutes: 45
+ steps:
+ - uses: actions/checkout@v7
+ - uses: ./.github/actions/install-frontend-dependencies
+ - name: Set up cargo cache
+ uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4
+ env:
+ NODE_OPTIONS: --no-deprecation
+ - name: Test the signing helper on pull requests
+ if: github.event_name == 'pull_request'
+ env:
+ APPLE_SIGNING_IDENTITY: "-" # Ad hoc signing requires no Apple credentials.
+ run: scripts/sign-macos.sh
+ - name: Run the signing helper with Apple credentials
+ if: github.event_name == 'push'
+ env:
+ APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
+ APPLE_NOTARIZATION_APPLE_ID: ${{ secrets.APPLE_NOTARIZATION_APPLE_ID }}
+ APPLE_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_NOTARIZATION_PASSWORD }}
+ APPLE_NOTARIZATION_TEAM_ID: ${{ secrets.APPLE_NOTARIZATION_TEAM_ID }}
+ P12_BASE64: ${{ secrets.APPLE_SIGNING_CERTIFICATE_P12_BASE64 }}
+ P12_PASSWORD: ${{ secrets.APPLE_SIGNING_CERTIFICATE_PASSWORD }}
+ run: |
+ set -euo pipefail
+ : "${APPLE_SIGNING_IDENTITY:?Missing APPLE_SIGNING_IDENTITY secret}"
+ : "${APPLE_NOTARIZATION_APPLE_ID:?Missing APPLE_NOTARIZATION_APPLE_ID secret}"
+ : "${APPLE_NOTARIZATION_PASSWORD:?Missing APPLE_NOTARIZATION_PASSWORD secret}"
+ : "${APPLE_NOTARIZATION_TEAM_ID:?Missing APPLE_NOTARIZATION_TEAM_ID secret}"
+ : "${P12_BASE64:?Missing APPLE_SIGNING_CERTIFICATE_P12_BASE64 secret}"
+ : "${P12_PASSWORD:?Missing APPLE_SIGNING_CERTIFICATE_PASSWORD secret}"
+ CERTIFICATE_PATH="$RUNNER_TEMP/signing.p12"
+ KEYCHAIN_PATH="$RUNNER_TEMP/signing.keychain-db"
+ umask 077
+ trap 'security delete-keychain "$KEYCHAIN_PATH"; rm -f "$CERTIFICATE_PATH" "$CERTIFICATE_PATH.pem"' EXIT
+ # Use PKCS12 algorithms supported by macOS Keychain.
+ printf '%s' "$P12_BASE64" | base64 -d > "$CERTIFICATE_PATH"
+ openssl pkcs12 -in "$CERTIFICATE_PATH" -passin env:P12_PASSWORD -nodes -out "$CERTIFICATE_PATH.pem"
+ openssl pkcs12 -export -in "$CERTIFICATE_PATH.pem" -passout env:P12_PASSWORD \
+ -keypbe PBE-SHA1-3DES -certpbe PBE-SHA1-3DES -macalg sha1 -out "$CERTIFICATE_PATH"
+ rm -f "$CERTIFICATE_PATH.pem"
+ security create-keychain -p "" "$KEYCHAIN_PATH"
+ security set-keychain-settings -lut 3600 "$KEYCHAIN_PATH"
+ security unlock-keychain -p "" "$KEYCHAIN_PATH"
+ security import "$CERTIFICATE_PATH" -k "$KEYCHAIN_PATH" -P "$P12_PASSWORD" -T /usr/bin/codesign
+ security set-key-partition-list -S apple-tool:,apple: -s -k "" "$KEYCHAIN_PATH" > /dev/null
+ security list-keychains -d user -s "$KEYCHAIN_PATH"
+ scripts/sign-macos.sh
+ - name: Run the signed binary
+ run: target/aarch64-apple-darwin/superoptimized/sqlpage --version
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 902b77b71..905779ef7 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -37,6 +37,55 @@ jobs:
- uses: ./.github/actions/install-frontend-dependencies
- name: Build
run: cargo build --profile superoptimized --locked --target ${{ matrix.target }} --features "${{ matrix.features }}"
+
+ # macOS: sign and notarize the binary
+ - name: Sign and notarize macOS binary
+ if: matrix.os == 'macos-latest'
+ env:
+ APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
+ APPLE_NOTARIZATION_APPLE_ID: ${{ secrets.APPLE_NOTARIZATION_APPLE_ID }}
+ APPLE_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_NOTARIZATION_PASSWORD }}
+ APPLE_NOTARIZATION_TEAM_ID: ${{ secrets.APPLE_NOTARIZATION_TEAM_ID }}
+ P12_BASE64: ${{ secrets.APPLE_SIGNING_CERTIFICATE_P12_BASE64 }}
+ P12_PASSWORD: ${{ secrets.APPLE_SIGNING_CERTIFICATE_PASSWORD }}
+ run: |
+ set -euo pipefail
+ BIN=target/${{ matrix.target }}/superoptimized/sqlpage
+ # Import the signing certificate into a temporary keychain
+ umask 077
+ trap 'security delete-keychain /tmp/signing.keychain; rm -f /tmp/signing.p12 /tmp/signing.pem' EXIT
+ # Use PKCS12 algorithms supported by macOS Keychain.
+ printf '%s' "$P12_BASE64" | base64 -d > /tmp/signing.p12
+ openssl pkcs12 -in /tmp/signing.p12 -passin env:P12_PASSWORD -nodes -out /tmp/signing.pem
+ openssl pkcs12 -export -in /tmp/signing.pem -passout env:P12_PASSWORD \
+ -keypbe PBE-SHA1-3DES -certpbe PBE-SHA1-3DES -macalg sha1 -out /tmp/signing.p12
+ rm -f /tmp/signing.pem
+ security create-keychain -p "" /tmp/signing.keychain
+ security set-keychain-settings -lut 3600 /tmp/signing.keychain
+ security unlock-keychain -p "" /tmp/signing.keychain
+ security import /tmp/signing.p12 -k /tmp/signing.keychain -P "$P12_PASSWORD" -T /usr/bin/codesign
+ security set-key-partition-list -S apple-tool:,apple: -s -k "" /tmp/signing.keychain > /dev/null
+ security list-keychains -d user -s /tmp/signing.keychain "$(security default-keychain)"
+ # Sign with hardened runtime and entitlements
+ codesign --force --options runtime --entitlements .github/macos/entitlements.plist --sign "$APPLE_SIGNING_IDENTITY" --timestamp "$BIN"
+ codesign --verify --deep --strict --verbose=2 "$BIN"
+ # Package in a zip and submit for notarization.
+ # Note: stapler only supports .app bundles, disk images, and flat packages —
+ # not bare Mach-O executables or zip archives. For a standalone binary,
+ # notarization alone is sufficient: Gatekeeper checks Apple's servers online.
+ ditto -c -k --keepParent "$BIN" sqlpage-macos.zip
+ xcrun notarytool submit sqlpage-macos.zip \
+ --apple-id "$APPLE_NOTARIZATION_APPLE_ID" \
+ --password "$APPLE_NOTARIZATION_PASSWORD" \
+ --team-id "$APPLE_NOTARIZATION_TEAM_ID" \
+ --wait --output-format plist > notarization-result.plist
+ if [[ "$(plutil -extract status raw -o - notarization-result.plist)" != "Accepted" ]]; then
+ echo "Notarization was not accepted:" >&2
+ cat notarization-result.plist >&2
+ exit 1
+ fi
+ codesign --verify --deep --strict "$BIN"
+
- name: Upload unsigned Windows artifact
if: matrix.os == 'windows-latest'
id: upload_unsigned
diff --git a/.gitignore b/.gitignore
index 1a2971ca6..e53db2d34 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,3 +15,17 @@ sqlpage/sqlpage.db
tests_uploads/
/test-results/
/playwright-report/
+
+# Local environment overrides (never commit secrets here!)
+# .env is tracked with safe defaults; use .env.local for secrets
+.env.local
+.env.*.local
+
+# Apple signing certificates (sensitive - do not commit!)
+*.cer
+*.p12
+*.csr
+developerID_application.cer
+sqlpage.key
+sqlpage.p12
+sqlpage_cert.pem
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 10ad050af..ff6cc86f6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,7 @@
# CHANGELOG.md
## v0.47.0 (unreleased)
+- **Mac users:** the downloadable `sqlpage-macos.tgz` is now code-signed with a Developer ID certificate and notarized by Apple. macOS may still ask you to confirm opening it the first time. The binary can still load third-party ODBC database drivers.
- **Mac users:** the downloadable `sqlpage-macos.tgz` now runs natively on Apple silicon (M-series Macs) and no longer runs on Intel Macs. Homebrew remains the recommended and easiest installation method. On an Intel Mac, [install Homebrew](https://brew.sh/) if needed, then run `brew install sqlpage` (or `brew update` followed by `brew upgrade sqlpage` if you already installed it with Homebrew). Open Terminal in your existing website folder and run `sqlpage` instead of `./sqlpage.bin`; keep your SQL files, database, and `sqlpage` configuration folder in place. Intel installations may build from source and take longer; see the [macOS installation guide](https://sql-page.com/your-first-sql-website/?os=macos#download) for setup and older macOS requirements.
- Chart data points can now include a `link`. Clicking a point or its tooltip value opens that URL; a text x value in the tooltip title links to it too. The tooltip remains open while the pointer moves onto the link. ApexCharts was updated to [v7.6.0](https://github.com/apexcharts/apexcharts.js/releases/tag/v7.6.0) to support this.
- Updated sqlx-oldapi to v0.6.57 to fix SQL Server fallback expressions such as `ISNULL($missing, 'default')` truncating defaults or failing for date values when the bound variable is `NULL`.
diff --git a/examples/official-site/your-first-sql-website/index.sql b/examples/official-site/your-first-sql-website/index.sql
index 9c2460e04..85c68cca8 100644
--- a/examples/official-site/your-first-sql-website/index.sql
+++ b/examples/official-site/your-first-sql-website/index.sql
@@ -43,7 +43,7 @@ Let''s create a simple website with a database from scratch, to learn SQLPage ba
ELSE 'https://github.com/sqlpage/SQLPage/releases'
END AS link,
CASE $os
- WHEN 'macos' THEN CONCAT('Install SQLPage ', $sqlpage_version, ' using Homebrew')
+ WHEN 'macos' THEN CONCAT('Install SQLPage ', $sqlpage_version, ' for macOS')
WHEN 'windows' THEN CONCAT('Download SQLPage ', $sqlpage_version, ' for Windows')
WHEN 'linux' THEN CONCAT('Download SQLPage ', $sqlpage_version, ' for Linux')
ELSE CONCAT('Download SQLPage ', $sqlpage_version)
diff --git a/examples/official-site/your-first-sql-website/tutorial-install-macos.md b/examples/official-site/your-first-sql-website/tutorial-install-macos.md
index 50daddf8f..973c21a9b 100644
--- a/examples/official-site/your-first-sql-website/tutorial-install-macos.md
+++ b/examples/official-site/your-first-sql-website/tutorial-install-macos.md
@@ -1,7 +1,27 @@
# Install SQLPage on macOS
-The recommended and easiest way to install SQLPage on macOS is [Homebrew](https://brew.sh/), including on Intel Macs.
-If you do not already have Homebrew, open Terminal and run:
+For a quick start on an Apple silicon Mac (M-series), download SQLPage below.
+If you already use [Homebrew](https://brew.sh/) or want easier updates, use the Homebrew instructions instead.
+On an Intel Mac, use Homebrew.
+
+## Download SQLPage for Apple silicon
+
+[Download SQLPage for Apple silicon](https://github.com/sqlpage/SQLPage/releases/latest/download/sqlpage-macos.tgz).
+Open the downloaded archive to extract it. Keep `sqlpage.bin` and the `sqlpage` folder together in your website folder.
+Open Terminal in that folder and run:
+
+```sh
+./sqlpage.bin
+```
+
+macOS may ask you to confirm opening SQLPage the first time.
+To update later, download the latest version and replace `sqlpage.bin`. Keep your SQL files, database, and `sqlpage` configuration folder.
+
+Starting with SQLPage v0.47.0, this download is for Apple silicon only. On an Intel Mac, use Homebrew below.
+
+## Install with Homebrew
+
+If you do not already have Homebrew, open Terminal and install it:
```sh
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
@@ -16,16 +36,13 @@ brew install sqlpage
Open Terminal in your website folder and run `sqlpage` to start your website.
To update SQLPage later, run `brew update` followed by `brew upgrade sqlpage`.
-**Using an Intel Mac?** Starting with SQLPage v0.47.0, the `sqlpage-macos.tgz` download is for Apple silicon (M-series Macs) only.
-Use Homebrew on Intel Macs. If you previously used the downloaded executable, keep your SQL files, database, and `sqlpage` configuration folder in place and run `sqlpage` from the same website folder instead of `./sqlpage.bin`.
-If SQLPage is already installed through Homebrew, use the upgrade commands above.
+If you previously used the downloaded executable, keep your SQL files, database, and `sqlpage` configuration folder in place and run `sqlpage` from the same website folder instead of `./sqlpage.bin`.
Homebrew may compile SQLPage and its dependencies from source on Intel Macs, so installation can take longer.
Source builds require Apple's Command Line Tools, which you can install with `xcode-select --install`.
Homebrew classifies Intel Macs as Tier 3 (limited support); older macOS versions also have restrictions. Check [Homebrew's macOS requirements](https://docs.brew.sh/Installation#macos-requirements) if installation fails.
> **Note**: Advanced users can alternatively install SQLPage using
-> [the precompiled binaries for Apple silicon](https://github.com/sqlpage/SQLPage/releases/latest),
> [docker](https://hub.docker.com/repository/docker/lovasoa/SQLPage/general),
> [nix](https://search.nixos.org/packages?channel=unstable&show=sqlpage),
> or [cargo](https://crates.io/crates/sqlpage).
diff --git a/examples/official-site/your-first-sql-website/tutorial.md b/examples/official-site/your-first-sql-website/tutorial.md
index 22cfc9f7e..761dfaa3b 100644
--- a/examples/official-site/your-first-sql-website/tutorial.md
+++ b/examples/official-site/your-first-sql-website/tutorial.md
@@ -5,7 +5,7 @@ In the rest of this tutorial, we will call this folder the **root folder** of yo
- On **Windows**, place the `sqlpage.exe` you downloaded above at the root of the folder. Then double-click the `sqlpage.exe` file to start the server.
- On **Linux**, place `sqlpage.bin` at the root of the folder. Then open a terminal, cd to the root folder of your website, and run `./sqlpage.bin` to start the server.
-- On **Mac OS**, if you installed SQLPage using Homebrew, then you do not need to place anything at the root of the folder. Open Terminal, cd to the root folder of your website, and type `sqlpage` to start the server.
+- On **Mac OS**, if you installed SQLPage using Homebrew, then you do not need to place anything at the root of the folder. Open Terminal, cd to the root folder of your website, and type `sqlpage` to start the server. If you downloaded SQLPage instead, place `sqlpage.bin` in this folder and run `./sqlpage.bin`.

diff --git a/scripts/sign-macos.sh b/scripts/sign-macos.sh
new file mode 100755
index 000000000..15b96d913
--- /dev/null
+++ b/scripts/sign-macos.sh
@@ -0,0 +1,109 @@
+#!/bin/bash
+# Script for local macOS signing and notarization testing
+#
+# Prerequisites:
+# 1. Build the frontend assets first: npm ci && npm run build
+# 2. Install the Rust target: rustup target add aarch64-apple-darwin
+# 3. Import your Developer ID certificate into the login keychain:
+# security import sqlpage.p12 -k ~/Library/Keychains/login.keychain-db -P "" -T /usr/bin/codesign
+#
+# Required environment variables for notarization:
+# APPLE_SIGNING_IDENTITY - e.g. "Developer ID Application: Your Name (TEAMID)", or "-" for ad hoc signing
+# APPLE_NOTARIZATION_APPLE_ID - Your Apple ID email
+# APPLE_NOTARIZATION_PASSWORD - App-specific password
+# APPLE_NOTARIZATION_TEAM_ID - Your 10-character Team ID
+
+set -euo pipefail
+
+# Check if we're on macOS
+if [[ "$(uname)" != "Darwin" ]]; then
+ echo "This script must be run on macOS"
+ exit 1
+fi
+
+# Check if required tools are available (use xcrun --find as tools may not be on PATH)
+if ! xcrun --find codesign &> /dev/null; then
+ echo "codesign not found. Please install Xcode command line tools:"
+ echo " xcode-select --install"
+ exit 1
+fi
+
+# Check that the signing identity is set
+if [[ -z "${APPLE_SIGNING_IDENTITY:-}" ]]; then
+ echo "APPLE_SIGNING_IDENTITY is not set."
+ echo "Example: export APPLE_SIGNING_IDENTITY=\"Developer ID Application: Your Name (TEAMID)\""
+ exit 1
+fi
+
+# Check that frontend assets exist (build.rs requires them)
+if [[ ! -f frontend/dist/tabler-sprite.svg ]]; then
+ echo "Frontend assets not found. Building them now..."
+ npm ci
+ npm run build
+fi
+
+# Install the ARM target if on Intel Mac
+if [[ "$(uname -m)" == "x86_64" ]]; then
+ echo "Intel Mac detected, ensuring aarch64-apple-darwin target is installed..."
+ rustup target add aarch64-apple-darwin
+fi
+
+# Build the binary
+echo "Building SQLPage for aarch64-apple-darwin..."
+cargo build --profile superoptimized --locked --target aarch64-apple-darwin --features "odbc-static"
+
+# Check if the binary exists
+BINARY_PATH="target/aarch64-apple-darwin/superoptimized/sqlpage"
+if [[ ! -f "$BINARY_PATH" ]]; then
+ echo "Binary not found at $BINARY_PATH"
+ exit 1
+fi
+
+# Sign the binary
+echo "Signing the binary..."
+SIGNING_TIMESTAMP_OPTION=--timestamp
+if [[ "$APPLE_SIGNING_IDENTITY" == "-" ]]; then
+ SIGNING_TIMESTAMP_OPTION=--timestamp=none
+fi
+codesign --force --options runtime --entitlements .github/macos/entitlements.plist --sign "$APPLE_SIGNING_IDENTITY" "$SIGNING_TIMESTAMP_OPTION" "$BINARY_PATH"
+
+# Verify the signature
+echo "Verifying the signature..."
+codesign --verify --deep --strict --verbose=2 "$BINARY_PATH"
+
+# Create a zip archive for notarization
+echo "Creating zip archive for notarization..."
+ditto -c -k --keepParent "$BINARY_PATH" sqlpage-macos.zip
+
+# Notarize the binary (if credentials are provided)
+# Note: stapler does not support bare Mach-O executables or zip archives.
+# For a standalone binary, notarization alone is sufficient — Gatekeeper
+# checks Apple's notarization servers online when the binary is first run.
+if [[ -n "${APPLE_NOTARIZATION_APPLE_ID:-}" && -n "${APPLE_NOTARIZATION_PASSWORD:-}" && -n "${APPLE_NOTARIZATION_TEAM_ID:-}" ]]; then
+ if ! xcrun --find notarytool &> /dev/null; then
+ echo "notarytool not found. Please install Xcode command line tools:"
+ echo " xcode-select --install"
+ exit 1
+ fi
+
+ echo "Submitting for notarization..."
+ xcrun notarytool submit sqlpage-macos.zip \
+ --apple-id "$APPLE_NOTARIZATION_APPLE_ID" \
+ --password "$APPLE_NOTARIZATION_PASSWORD" \
+ --team-id "$APPLE_NOTARIZATION_TEAM_ID" \
+ --wait --output-format plist > notarization-result.plist
+ if [[ "$(plutil -extract status raw -o - notarization-result.plist)" != "Accepted" ]]; then
+ echo "Notarization was not accepted:" >&2
+ cat notarization-result.plist >&2
+ exit 1
+ fi
+
+ # Verify the signature and notarization ticket for the standalone binary.
+ echo "Final verification..."
+ codesign --verify --deep --strict --verbose=2 --check-notarization -R=notarized "$BINARY_PATH"
+else
+ echo "Skipping notarization. Set APPLE_NOTARIZATION_APPLE_ID, APPLE_NOTARIZATION_PASSWORD, and APPLE_NOTARIZATION_TEAM_ID to enable notarization."
+ echo "Note: The binary is signed but not notarized. Gatekeeper will still show a warning."
+fi
+
+echo "macOS signing complete!"