Multi-tenant Android app + home-screen widget for any Luach-powered shul. One APK serves every shul on the registry — the user picks which one when they install. Built around Jetpack Glance for the widget, Jetpack Compose for the full app shell, Supabase for data, and KosherJava for halachic times.
Current release: v3.1.2 — install from Shul-Widget-Published-App releases. Supports Android 7.1.1 (API 25) and up.
A lineup of home-screen widgets, each responsive across size brackets via Glance SizeMode.Responsive:
- Full Day — the full chronological day with a zmanim strip. Full Screen — edge-to-edge day view.
- Next Up — the next item on the schedule. Next Zman — the upcoming zman with a live countdown.
- Shabbos — candle lighting, parsha, and motzei. Weather — current conditions, hourly strip, 5-day outlook.
- Zmanim — the user's selected zmanim set. Custom — per-instance configurable content.
- Always-light cream surface with navy + gold text — readable against any home-screen wallpaper.
- Refreshes every 30 minutes via WorkManager, plus instantly on boot / screen-on / date change.
Tap the widget (or the launcher icon) to open the full app. Seven tabs:
| Tab | What |
|---|---|
| Today | Chronological list of today's minyanim, shiurim, and events with resolved times. Pull-to-refresh. Tap the calendar to view any future date. |
| Schedule | Recurring weekly view per tefilla (Shacharis / Mincha / Maariv). |
| Zmanim | All zmanim selected in Settings — Alos, Misheyakir, sunrise, sunset, plag, tzais, candle lighting, etc. |
| Weather | Open-Meteo forecast — current conditions, 5-day outlook, tap a day for hourly breakdown + sunrise/sunset. |
| Admin | Sign in with Google or email/password to manage minyanim, shiurim, events, exceptions, and settings for your shul. The login form is always visible; signed-in users see the CRUD shell. |
| Chat (admins only) | Per-shul team chat with OS notifications via a background poll worker. |
| Settings | Switch shul, refresh schedule, check for updates, customize zmanim, toggle item-type icons, links to kiosk preview + web admin + GitHub + contact + bug report. |
- Supabase REST direct from the device — every shul has its own Supabase project; the registry table in the master project hands the URL + anon key + table prefix to the picker, then the rest of the app talks to the selected shul.
- RLS-gated writes —
shul_<prefix>_userstable with roleowner/admindecides who can edit; the bearer token comes from either email/password sign-in or the Google OAuth flow (v1.1.0). - Stale-while-revalidate caching (v1.0.7) — Today, Zmanim, and Schedule paint the cached snapshot instantly on tab-switch while the network refresh runs in the background.
- KosherJava on-device for sunrise / sunset / candle lighting / tzais / plag / mincha gedola / etc. — driven by the chosen shul's lat / lon / timezone.
- Polls
https://api.github.com/repos/Shalom-Karr/Shul-Widget-Published-App/releasesevery 6 hours, or on demand from Settings → Check for updates. - ABI-aware: the in-app updater picks the matching APK slice (
armeabi-v7a/arm64-v8a/x86_64/ universal) so a TracFone-class device doesn't download more than it needs. - Self-installs via
ACTION_VIEWon Android < 8 (reliable confirm dialog) andPackageInstaller.Sessionon Android 8+ (catchesSTATUS_PENDING_USER_ACTIONto fire the install prompt).
Shul-Widget/ ← this repo
├── app/
│ ├── build.gradle.kts ← deps + ABI splits + R8 + signing
│ ├── proguard-rules.pro
│ └── src/main/
│ ├── AndroidManifest.xml ← receivers, FileProvider, OAuth deep link
│ ├── kotlin/com/luach/widget/
│ │ ├── ShulWidget.kt ← Glance widget composable
│ │ ├── ShulWidgetReceiver.kt
│ │ ├── ShulWidgetApp.kt ← Application class
│ │ ├── app/ ← Compose full-app shell
│ │ │ ├── MainActivity.kt ← single-activity host + OAuth deep-link handler
│ │ │ ├── AppNavHost.kt ← bottom-nav routes + Admin tab visibility
│ │ │ └── screens/{today,schedule,zmanim,weather,admin,chat,settings}/
│ │ ├── auth/
│ │ │ ├── SupabaseAuthClient.kt ← password + OAuth (authorize URL + /user)
│ │ │ ├── OAuthLauncher.kt ← Chrome Custom Tabs handoff
│ │ │ ├── AdminSession.kt
│ │ │ └── RbacChecker.kt ← shul_<prefix>_users gate
│ │ ├── chat/
│ │ │ └── ChatPollWorker.kt ← periodic poll + OS notifications
│ │ ├── config/
│ │ │ ├── ShulPickerActivity.kt ← picker + widget configure activity
│ │ │ └── ShulPrefs.kt
│ │ ├── data/
│ │ │ ├── Models.kt
│ │ │ ├── RegistryClient.kt
│ │ │ ├── SupabaseClient.kt ← per-shul REST + admin CRUD
│ │ │ ├── ScheduleResolver.kt ← day_pattern + anchor logic, SWR flow
│ │ │ └── LocalCache.kt
│ │ ├── kiosk/
│ │ │ └── KioskPreviewActivity.kt
│ │ ├── refresh/ ← BootReceiver, TimeTickReceiver, WorkManager
│ │ ├── update/ ← UpdateChecker, ApkInstaller, InstallStatusReceiver
│ │ ├── weather/ ← OpenMeteoClient, WeatherCache, models
│ │ ├── widget/ ← WeatherWidget + WeatherWidgetReceiver
│ │ └── zmanim/ ← ZmanimProvider (KosherJava wrapper), ZmanimKey
│ └── res/ ← layouts, drawables, mipmaps, strings, xml configs
├── supabase/ ← run in the master project (registry) — migrations
│ ├── 01_shuls_registry.sql
│ ├── 02_seed_shuls.sql
│ ├── 03_add_kiosk_url.sql
│ ├── 04_add_admin_url.sql
│ ├── 05_install_telemetry.sql
│ └── registry_add_latlon.sql
├── .github/workflows/
│ ├── android-apk.yml ← debug APK on every push, uploads artifact
│ └── release-to-public.yml ← on tag: waits for build, attaches APKs to public release
├── SIGNING.md
└── README.md
Shul-Widget(this repo) — source code, CI workflows, migrations.Shul-Widget-Published-App(public) — release page, CHANGELOG, landing site, APK downloads. The in-app updater only ever talks to this public repo.
The release-to-public.yml workflow builds the APKs in Shul-Widget on tag push, waits for the Android build to finish (lewagon/wait-on-check-action), then pushes a release + CHANGELOG entry to Shul-Widget-Published-App.
- Drop the widget on the home screen → Android opens
ShulPickerActivity. - Picker calls
RegistryClient.fetchShuls()against the master Supabase project. - User taps a shul →
{slug, supabaseUrl, supabaseAnonKey, tablePrefix, kioskUrl, adminUrl, logoUrl}saved to DataStore. WidgetRefreshWorkeris scheduled (30 min periodic) and fires immediately.
chosen shul (DataStore)
│
▼
SupabaseClient ──parallel──▶ getSettings / getMinyanim / getShiurim
│ / getMinyanExceptions(today) / getShiurExceptions(today)
│ / getEvents / getEventExceptions(today)
│
▼
LocalCache.save(snapshot) ←── stale-while-revalidate: cache emit first, then fresh
│
▼
ScheduleResolver: match day_pattern(today, jCal) → resolve anchor + offset_minutes via ZmanimProvider
│
▼
FullDaySchedule { minyanim, shiurim, events, zmanim, hebrewDate }
│
▼
Glance widget render + Today / Schedule / Zmanim screens
UpdateChecker (in the periodic worker, plus manual from Settings):
- GETs
/releases?per_page=1on the public repo (the/releases/latestendpoint silently 404s if everything is a pre-release, which once stranded the v0.0.7 → v1.0.0 transition). - Compares the tag against
PackageManager.getPackageInfo(packageName).versionName(v1.0.9+: was previously reflecting onBuildConfig.VERSION_NAME, which broke withapplicationIdSuffix = ".debug"). - Picks the asset whose name contains the device's primary ABI; falls back to the universal APK.
- Downloads to app-private storage →
ApkInstallershows the system install confirmation.
Requires JDK 17 + Android SDK (build-tools 34.0.0, platform 34, platform-tools). No Android Studio.
# One-time
gradle wrapper --gradle-version 8.7
# Every build
./gradlew :app:assembleDebug
# Output:
# app/build/outputs/apk/debug/app-armeabi-v7a-debug.apk
# app/build/outputs/apk/debug/app-arm64-v8a-debug.apk
# app/build/outputs/apk/debug/app-x86_64-debug.apk
# app/build/outputs/apk/debug/app-universal-debug.apkSideload to a connected device:
adb install -r app/build/outputs/apk/debug/app-armeabi-v7a-debug.apkCI on push is usually faster (~3–5 min) — see the Actions tab.
Push a tag matching v*:
git tag v1.1.1
git push origin main
git push origin v1.1.1The pipeline:
android-apk.ymlbuilds the four ABI APKs on push to main.release-to-public.ymlfires on the tag, waits for the matching build (lewagon/wait-on-check-actionpolls Checks API for thisgithub.sha), then:- Renames
app-<abi>-debug.apk→shul-widget-<tag>-<abi>.apk(the universal APK keeps the legacyshul-widget-<tag>.apkfilename so older clients with naïveendsWith(".apk")matchers still upgrade safely). - Prepends a CHANGELOG entry in the public repo and stamps the
index.htmldownload CTA. - Creates the GitHub release on the public repo and attaches all four APKs.
- Renames
Keystore: debug signing right now (committed debug.keystore for stable signature across CI runs). See SIGNING.md for release-keystore setup if/when we switch.
v1.1.0 added a "Sign in with Google" button to the Admin tab. The flow is Supabase-mediated (no Google Play Services dependency).
Per Supabase project (each shul):
- Supabase Studio → Authentication → Providers → Google → enable.
- Paste in a Web OAuth Client ID + secret from Google Cloud Console.
- Authorized redirect URI on the Google client must be the Supabase callback shown in Studio:
https://<project-ref>.supabase.co/auth/v1/callback. - In Supabase Studio → Authentication → URL Configuration → Additional Redirect URLs, add:
luach://auth/callback. The Android intent-filter onMainActivitycatches this scheme.
On Android:
- App opens a Chrome Custom Tab at
<supabaseUrl>/auth/v1/authorize?provider=google&redirect_to=luach://auth/callback. - After Google consent, Supabase 302s the browser to
luach://auth/callback#access_token=…&refresh_token=…&expires_in=…. MainActivity.onNewIntentparses the URL fragment, hands the tokens toSettingsViewModel.handleOAuthCallback, which calls/auth/v1/userfor the userId, does the RBAC check, and persists the session.
Email/password sign-in is still available as a fallback below the Google button.
Run in the registry-holder Supabase project (currently CMH/Khal: qvoxpfigbukidlmshiei.supabase.co):
supabase/01_shuls_registry.sql— registry table + public-read RLS.supabase/02_seed_shuls.sql— TLK, Khal Yereim, CMH rows.
Per-shul Supabase projects need their own schema (the shul_<prefix>_* tables) plus the shul_<prefix>_users RLS gate that decides who can edit.
| File | What |
|---|---|
gradle.properties |
shulWidget.versionCode / shulWidget.versionName — single source of truth, baked into the APK manifest. |
app/build.gradle.kts |
splits.abi (ABIs to ship), buildTypes.debug (R8 settings), signing configs. |
app/proguard-rules.pro |
Keep rules — receivers, activities, kotlinx.serialization, BuildConfig. |
app/src/main/res/xml/*_widget_info.xml |
Per-widget min/max resize, configure activity, target cell size (fullscreen_, nextup_, nextzman_, shabbos_, weather_, zmanim_). |
app/src/main/AndroidManifest.xml |
OAuth deep-link intent filter (luach://auth/callback), permissions, receivers. |
- The "debug" build type is also
debuggable=true, which silently disables R8. APK is ~8 MB instead of ~5 MB it could be. Switching to a true release build is a future change — it requires CI keystore secrets and breaks the in-place upgrade path becauseapplicationIdSuffix=".debug"would go away.
No license has been chosen yet, so the default applies: all rights reserved. The source is public for reference; add a LICENSE file to grant reuse rights.