Matthias Nott
2026-06-11 54a1fd0ffbd9b663bd9aa502009ea54faae3b304
build: store-release pipeline (App Store + Play Store), Android release signing

Ports Glidr's release mechanism into PAILot — same Apple Dev Team (7KU642K5ZL),
same App Store Connect API key path, same JDK 21 pin, same project-local key-
store pattern. PAILot's pipeline is smaller than Glidr's (no content sync, no
HMAC rotation): just iOS IPA build + altool upload, plus Android AAB build.

- tools/build-appstore.sh: build IPA, fix archive name, upload via xcodebuild
-exportArchive with the App Store Connect auth key.
- tools/build-android-aab.sh: build a Play Store-ready AAB (pinned to JDK 21).
- tools/release.sh: unified runner — iOS then Android, with --ios-only /
--android-only flags.
- tools/privacy.html: privacy policy adapted for PAILot's BYO-server model
(no backend, no analytics, no third-party tracking, audio/text/tokens all
go to the user's own AIBroker daemon).
- android/app/build.gradle.kts: real release signing config (keystore at
android/release.keystore, alias 'pailot'), R8 minification on.
- .gitignore: never commit the keystore.

Keystore generated at android/release.keystore. Back it up — losing it means
losing the ability to publish updates under com.tekmidian.pailot.
2 files modified
4 files added
changed files
.gitignore patch | view | blame | history
android/app/build.gradle.kts patch | view | blame | history
tools/build-android-aab.sh patch | view | blame | history
tools/build-appstore.sh patch | view | blame | history
tools/privacy.html patch | view | blame | history
tools/release.sh patch | view | blame | history
.gitignore
....@@ -43,3 +43,7 @@
4343 /android/app/debug
4444 /android/app/profile
4545 /android/app/release
46
+
47
+# Release signing — never commit the keystore.
48
+android/release.keystore
49
+android/key.properties
android/app/build.gradle.kts
....@@ -20,21 +20,30 @@
2020 }
2121
2222 defaultConfig {
23
- // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
2423 applicationId = "com.tekmidian.pailot"
25
- // You can update the following values to match your application needs.
26
- // For more information, see: https://flutter.dev/to/review-gradle-config.
2724 minSdk = flutter.minSdkVersion
2825 targetSdk = flutter.targetSdkVersion
2926 versionCode = flutter.versionCode
3027 versionName = flutter.versionName
3128 }
3229
30
+ // Release signing — keystore lives at android/release.keystore (gitignored).
31
+ // Back this file up: losing it means losing the ability to ship updates to
32
+ // the Play Store under this package name.
33
+ signingConfigs {
34
+ create("release") {
35
+ storeFile = file("../release.keystore")
36
+ storePassword = "pailot-release-key-2026"
37
+ keyAlias = "pailot"
38
+ keyPassword = "pailot-release-key-2026"
39
+ }
40
+ }
41
+
3342 buildTypes {
3443 release {
35
- // TODO: Add your own signing config for the release build.
36
- // Signing with the debug keys for now, so `flutter run --release` works.
37
- signingConfig = signingConfigs.getByName("debug")
44
+ signingConfig = signingConfigs.getByName("release")
45
+ isMinifyEnabled = true
46
+ proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"))
3847 }
3948 }
4049 }
tools/build-android-aab.sh
....@@ -0,0 +1,22 @@
1
+#!/bin/bash
2
+# PAILot Android AAB Build Script
3
+# Produces a Play Store-ready signed App Bundle.
4
+set -e
5
+
6
+SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
7
+APP_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
8
+cd "$APP_DIR"
9
+
10
+# Pin JDK 21 — system openjdk 25 is not yet parseable by the Kotlin compiler
11
+# bundled with AGP. JDK 17 also works if you prefer; just point JAVA_HOME at it.
12
+JDK="${JAVA_HOME_RELEASE:-/opt/homebrew/Cellar/openjdk@21/21.0.11/libexec/openjdk.jdk/Contents/Home}"
13
+
14
+echo "=== Building Android App Bundle (AAB) ==="
15
+JAVA_HOME="$JDK" flutter build appbundle --release --no-tree-shake-icons
16
+
17
+echo ""
18
+echo "=== Done ==="
19
+AAB=build/app/outputs/bundle/release/app-release.aab
20
+ls -la "$AAB"
21
+echo ""
22
+echo "Upload $AAB to the Play Console (Production → Create new release)."
tools/build-appstore.sh
....@@ -0,0 +1,82 @@
1
+#!/bin/bash
2
+# PAILot App Store Build Script
3
+# Builds an iOS IPA and uploads it to App Store Connect.
4
+# Adapted from Glidr's pipeline — same Apple Dev Team (7KU642K5ZL).
5
+set -e
6
+
7
+SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
8
+APP_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
9
+cd "$APP_DIR"
10
+
11
+# App Store Connect API credentials (issuer is shared across keys for this team).
12
+ASC_KEY_ID="${ASC_KEY_ID:-4H68LK2M3J}"
13
+ASC_ISSUER_ID="${ASC_ISSUER_ID:-69a6de75-2050-47e3-e053-5b8c7c11a4d1}"
14
+ASC_KEY_PATH="${ASC_KEY_PATH:-$HOME/.appstoreconnect/private_keys/AuthKey_${ASC_KEY_ID}.p8}"
15
+TEAM_ID="7KU642K5ZL"
16
+
17
+if [ ! -f "$ASC_KEY_PATH" ]; then
18
+ echo "ERROR: App Store Connect API key not found at $ASC_KEY_PATH"
19
+ echo " Download from https://appstoreconnect.apple.com/access/api"
20
+ exit 1
21
+fi
22
+
23
+# Homebrew rsync breaks Xcode's export step — temporarily hide it.
24
+BREW_RSYNC="$(brew --prefix 2>/dev/null)/bin/rsync"
25
+RSYNC_MOVED=false
26
+if [ -f "$BREW_RSYNC" ]; then
27
+ mv "$BREW_RSYNC" "${BREW_RSYNC}.bak"
28
+ RSYNC_MOVED=true
29
+ echo " Temporarily hid Homebrew rsync"
30
+fi
31
+cleanup() {
32
+ if [ "$RSYNC_MOVED" = true ] && [ -f "${BREW_RSYNC}.bak" ]; then
33
+ mv "${BREW_RSYNC}.bak" "$BREW_RSYNC"
34
+ echo " Restored Homebrew rsync"
35
+ fi
36
+}
37
+trap cleanup EXIT
38
+
39
+echo "=== Building IPA for App Store ==="
40
+flutter build ipa --release --export-method development --no-tree-shake-icons
41
+
42
+echo ""
43
+echo "=== Fixing archive name ==="
44
+ARCHIVE="build/ios/archive/Runner.xcarchive/Info.plist"
45
+if [ -f "$ARCHIVE" ]; then
46
+ plutil -replace Name -string "PAILot" "$ARCHIVE"
47
+ plutil -replace SchemeName -string "PAILot" "$ARCHIVE"
48
+ echo " Archive name set to 'PAILot'"
49
+fi
50
+
51
+echo ""
52
+echo "=== Exporting and uploading to App Store Connect ==="
53
+cat > /tmp/PAILotAppStoreExport.plist <<PLIST
54
+<?xml version="1.0" encoding="UTF-8"?>
55
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
56
+<plist version="1.0">
57
+<dict>
58
+ <key>method</key>
59
+ <string>app-store-connect</string>
60
+ <key>destination</key>
61
+ <string>upload</string>
62
+ <key>signingStyle</key>
63
+ <string>automatic</string>
64
+ <key>teamID</key>
65
+ <string>${TEAM_ID}</string>
66
+</dict>
67
+</plist>
68
+PLIST
69
+
70
+rm -rf /tmp/pailot-appstore-export 2>/dev/null
71
+xcodebuild -exportArchive \
72
+ -archivePath build/ios/archive/Runner.xcarchive \
73
+ -exportOptionsPlist /tmp/PAILotAppStoreExport.plist \
74
+ -exportPath /tmp/pailot-appstore-export \
75
+ -allowProvisioningUpdates \
76
+ -authenticationKeyPath "$ASC_KEY_PATH" \
77
+ -authenticationKeyID "$ASC_KEY_ID" \
78
+ -authenticationKeyIssuerID "$ASC_ISSUER_ID"
79
+
80
+echo ""
81
+echo "=== Done ==="
82
+echo "Uploaded to App Store Connect. Next: complete the build's metadata and submit for review."
tools/privacy.html
....@@ -0,0 +1,95 @@
1
+<!DOCTYPE html>
2
+<html lang="en">
3
+<head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <meta name="description" content="Privacy Policy for the PAILot voice-first AI communicator app">
7
+ <title>Privacy Policy - PAILot</title>
8
+ <style>
9
+ * { margin: 0; padding: 0; box-sizing: border-box; }
10
+ body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; line-height: 1.6; color: #333; background-color: #f9f9f9; }
11
+ .container { max-width: 900px; margin: 0 auto; padding: 20px; background-color: #ffffff; }
12
+ header { border-bottom: 2px solid #007AFF; padding-bottom: 20px; margin-bottom: 30px; }
13
+ h1 { font-size: 2.5rem; color: #007AFF; margin-bottom: 10px; }
14
+ .subtitle { color: #666; font-size: 1rem; }
15
+ .last-updated { color: #999; font-size: 0.9rem; margin-top: 10px; }
16
+ h2 { font-size: 1.5rem; color: #007AFF; margin-top: 35px; margin-bottom: 15px; }
17
+ h3 { font-size: 1.1rem; color: #333; margin-top: 20px; margin-bottom: 10px; }
18
+ p { margin-bottom: 15px; text-align: justify; }
19
+ ul { margin-left: 30px; margin-bottom: 15px; }
20
+ li { margin-bottom: 8px; }
21
+ .highlight { background-color: #f0f8ff; border-left: 4px solid #007AFF; padding: 15px; margin: 20px 0; }
22
+ footer { margin-top: 40px; padding-top: 20px; border-top: 1px solid #eee; color: #666; font-size: 0.9rem; text-align: center; }
23
+ @media (max-width: 600px) { .container { padding: 15px; } h1 { font-size: 2rem; } h2 { font-size: 1.3rem; } }
24
+ </style>
25
+</head>
26
+<body>
27
+ <div class="container">
28
+ <header>
29
+ <h1>Privacy Policy</h1>
30
+ <p class="subtitle">PAILot — voice-first AI communicator</p>
31
+ <p class="last-updated">Last updated: June 2026</p>
32
+ </header>
33
+
34
+ <h2>Introduction</h2>
35
+ <p>PAILot is a client application that connects to a personal AI infrastructure server (AIBroker) that you operate. PAILot is intentionally designed so that we, TEKmidian, do not host, store, or process your data on our infrastructure. The app's purpose is to relay your voice and text messages between you and the Claude Code sessions running on your own machine.</p>
36
+
37
+ <div class="highlight">
38
+ <strong>Plain English:</strong> PAILot talks to a server you run yourself. We don't see your messages, your voice, or your conversations. There is no PAILot cloud.
39
+ </div>
40
+
41
+ <h2>What PAILot Does Not Do</h2>
42
+ <ul>
43
+ <li>We do not operate a backend. The app connects only to the AIBroker daemon you have configured.</li>
44
+ <li>We do not collect or store any of your messages, voice recordings, transcripts, or session data.</li>
45
+ <li>We do not use third-party analytics SDKs (no Firebase Analytics, no Crashlytics by default, no advertising IDs).</li>
46
+ <li>We do not track you across apps or websites.</li>
47
+ <li>We do not sell or share any data with third parties.</li>
48
+ </ul>
49
+
50
+ <h2>What the App Processes Locally and Sends to Your Server</h2>
51
+
52
+ <h3>Voice recordings</h3>
53
+ <p>When you record a voice message, the audio is captured locally on your device and sent over an encrypted MQTT connection to <em>your own</em> AIBroker daemon. Transcription happens on that machine. Neither the audio nor the transcript leaves your infrastructure.</p>
54
+
55
+ <h3>Text messages and screenshots</h3>
56
+ <p>Text messages and screenshots you send are transmitted directly to your AIBroker daemon. They are not routed through any TEKmidian server.</p>
57
+
58
+ <h3>Push notification tokens</h3>
59
+ <p>To deliver notifications when the app is in the background, your device's APNs token (iOS) or FCM token (Android) is sent to your AIBroker daemon, which uses it to push notifications via Apple or Google. The token never reaches TEKmidian.</p>
60
+
61
+ <h3>Local storage on your device</h3>
62
+ <p>The app stores conversation history, session names, unread counts, and configuration locally on the device using the operating system's standard private app sandbox. This data is removed when you uninstall the app.</p>
63
+
64
+ <h2>Permissions the App Requests</h2>
65
+ <ul>
66
+ <li><strong>Microphone</strong> — required to record voice messages. Used only when you actively initiate a recording.</li>
67
+ <li><strong>Notifications</strong> — required to alert you to incoming messages.</li>
68
+ <li><strong>Camera (optional)</strong> — used only when you explicitly attach a photo.</li>
69
+ <li><strong>Network</strong> — required to communicate with your AIBroker server.</li>
70
+ </ul>
71
+
72
+ <h2>Third-Party Services Embedded in the App</h2>
73
+ <p>The app relies on the operating system's push notification systems (Apple Push Notification service and Firebase Cloud Messaging) <em>solely</em> as a delivery channel. These platforms have their own privacy policies governing notification delivery, which apply when your AIBroker server sends a push through them.</p>
74
+
75
+ <h2>Data Retention</h2>
76
+ <p>We retain none of your data because we never receive any. Anything stored on your device is governed by your device's privacy and backup settings. Data on your AIBroker server is governed by your own decisions about how you run that server.</p>
77
+
78
+ <h2>Children's Privacy</h2>
79
+ <p>PAILot is not directed at children under 13. We do not knowingly collect information from children.</p>
80
+
81
+ <h2>Changes to This Privacy Policy</h2>
82
+ <p>If this policy materially changes, the updated version will be available at the same URL and announced in the app's release notes. The "Last updated" date above will reflect the most recent change.</p>
83
+
84
+ <h2>Your Rights</h2>
85
+ <p>Because we do not process your personal data, GDPR/CCPA "access, correction, deletion" requests do not produce anything on our side. To remove all locally stored data, uninstall the app. To remove data held on your AIBroker server, manage it directly on the machine where that server runs.</p>
86
+
87
+ <h2>Contact Us</h2>
88
+ <p>If you have questions about this policy, contact us at <a href="mailto:support@tekmidian.com">support@tekmidian.com</a>.</p>
89
+
90
+ <footer>
91
+ <p>&copy; 2026 TEKmidian. All rights reserved.</p>
92
+ </footer>
93
+ </div>
94
+</body>
95
+</html>
tools/release.sh
....@@ -0,0 +1,59 @@
1
+#!/bin/bash
2
+# PAILot full-store release pipeline.
3
+# Builds iOS IPA, uploads to App Store Connect, then builds Android AAB.
4
+#
5
+# Prereqs:
6
+# - Apple Dev Team 7KU642K5ZL provisioning set up (same as Glidr).
7
+# - App Store Connect API key at $HOME/.appstoreconnect/private_keys/AuthKey_*.p8
8
+# - android/release.keystore present (generated once; back this up!).
9
+#
10
+# Usage:
11
+# ./tools/release.sh # iOS + Android
12
+# ./tools/release.sh --ios-only # skip Android
13
+# ./tools/release.sh --android-only # skip iOS
14
+set -e
15
+
16
+SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
17
+APP_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
18
+cd "$APP_DIR"
19
+
20
+IOS=true
21
+ANDROID=true
22
+for arg in "$@"; do
23
+ case "$arg" in
24
+ --ios-only) ANDROID=false ;;
25
+ --android-only) IOS=false ;;
26
+ esac
27
+done
28
+
29
+# Kill any competing flutter/dart processes (other sessions hold locks).
30
+pkill -f "dartvm.*flutter_tools" 2>/dev/null || true
31
+sleep 1
32
+
33
+APP_VERSION=$(grep "^version:" pubspec.yaml | sed 's/version: //')
34
+echo "========================================="
35
+echo " PAILot store release v${APP_VERSION}"
36
+echo "========================================="
37
+echo ""
38
+
39
+if $IOS; then
40
+ echo "[iOS] Building IPA and uploading to App Store Connect..."
41
+ bash tools/build-appstore.sh
42
+ echo ""
43
+fi
44
+
45
+if $ANDROID; then
46
+ echo "[Android] Building AAB..."
47
+ bash tools/build-android-aab.sh
48
+ echo ""
49
+fi
50
+
51
+echo "========================================="
52
+echo " RELEASE COMPLETE"
53
+echo "========================================="
54
+echo ""
55
+$IOS && echo "iOS: Uploaded to App Store Connect."
56
+$ANDROID && echo "Android: build/app/outputs/bundle/release/app-release.aab"
57
+$ANDROID && echo " Upload to Google Play Console manually."
58
+echo ""
59
+echo "Next: bump the build number on each store and submit for review."