This file is for coding agents working on Searchy. Keep the project minimal, private by design, and production-minded.
- Searchy is one Go service (module
searchy). - It searches images and videos through a self-hosted SearXNG instance over its JSON API.
- Telegram uses long polling; the bot makes only outbound connections (no public routes).
- Two Postgres connections are used and both are optional:
DATABASE_URLβ searchy's own private search analytics.CORE_DATABASE_URLβ the shared cross-bot core hub (identity, presence, language).
- Search itself is stateless and cache-fronted; nothing about a query is written to disk.
- Two media categories only: images and videos (
i:/v:prefixes). - Inline search is the primary surface; DM and group search use a numbered grid.
- The
/startmenu is inline and callback-driven (language, stats, help, about). - In groups, searching is explicit via
/search; plain messages are ignored. - Do not add audio/music search, webhook mode, or search/delivery surfaces beyond these unless explicitly requested.
internal/vidois a production least-privilege bridge, not a downloader: Searchy creates owner-bound intents and executes validatedDeliveryPlan v1operations, while Vido owns URLs, settings, extraction, shared-cache writes, error classification, and Vido-DM delivery.WEBHOOK_SECRETremains an unwired placeholder.
- Never store the query text β not in the analytics DB, not in logs, not in in-memory grid sessions. Only counts, timings, category, result type, and the SearXNG engine may be recorded.
- Analytics rows key on the raw Telegram user id with no query, title, or URL.
- Do not log the query text anywhere, including on errors or degraded-engine warnings.
- Do not recreate a local
userstable. Identity, presence, and the user's saved language live in the shared core Postgres, keyed on the global Telegram id. The startup schema drops any legacyuserstable on purpose. - Searchy identifies itself to core as
searchyand connects with a least-privilege role (searchy_core). - Write to core only through its SECURITY DEFINER functions (
core.touch,core.set_language,core.clear_language); read language throughcore.effective_language. Do not read or write core tables directly. - Every
db.Storeandcore.Coremethod must stay nil-safe: an unset or unreachable database makes the call a harmless no-op so search keeps working.
- Query with a pinned
engines=set; never sendcategories(it fans out to every enabled engine and overloads a private instance). - Validate every inline media/button URL as strict HTTPS before answering β one
bad URL rejects the whole
answerInlineQuery. - Surface a
403onformat=jsonas a clear "enable JSON format" error. - Keep the SearXNG instance internal-only;
IMAGE_PROXYstaysfalseunless the instance itself is publicly reachable over HTTPS.
- Prefer small packages and simple interfaces.
- Use the standard library unless a dependency materially reduces complexity.
- Keep SQL explicit and close to the store method that uses it.
- Avoid broad refactors while changing behavior.
- Add comments only when they explain non-obvious privacy, concurrency, or protocol behavior.
- User-facing strings go through
internal/i18n; escape user-controlled text before Telegram HTML formatting.
- The analytics schema is
internal/db/schema.sql, applied on startup and written to be idempotent (CREATE ... IF NOT EXISTS, guardedALTER). - Do not add a query-text column to
searchesorselections. - Keep DDL bounded by
lock_timeout/statement_timeoutso startup can't wedge on a held lock.
- Two branches: work on
dev(development), publish releases frommain. The## Unreleasedchangelog section tracks what has merged todevbut not yet shipped. Seedocs/versioning.md. - Follow
docs/versioning.mdfor release tags. - Keep the first release line as
v0.1.0-alpha.1,v0.1.0-beta.1,v0.1.0-rc.1, thenv0.1.0. - Use patch versions for fixes, minor versions for MVP-compatible product or
operational improvements, and reserve
v1.0.0for a stable production contract. - Treat required env vars, SearXNG requirements, the analytics schema, the core
contract, and deployment assumptions as breaking-sensitive before
v1.0.0.
- Keep
CHANGELOG.mdupdated for notable user-visible, operational, security, schema, and behavior changes. - Add unreleased notes under
## Unreleased; move them into a version section only when preparing the tag. - Follow
docs/releases.mdfor changelog sections, release note shape, and GitHub Release commands. - Mark
alpha,beta, andrcGitHub Releases as pre-releases. - Do not publish a GitHub Release until verification results and release notes match the tagged code.
The local machine may not have go in PATH. Use Docker for verification:
docker run --rm -v "$PWD":/src -w /src golang:1.26-alpine go test ./...Run vet when changing service logic:
docker run --rm -v "$PWD":/src -w /src golang:1.26-alpine go vet ./...For Docker/compose changes, also run:
docker compose -f deploy/docker-compose.yml config/startopens the menu in a private chat and in a group.- Inline search returns image and video cards and pages with
next_offset. - DM plain-text search and group
/searchboth return a numbered grid; paging and per-item pick work. - Category prefixes (
i:/v:) narrow correctly. - A SearXNG
403(JSON off) surfaces as a clear error, not a silent empty. - Language picker persists a choice and it survives a restart (via core).
/statsrenders personal and global tabs whenDATABASE_URLis set, and the bot runs cleanly with both databases unset.- No query text appears in analytics or logs.
/healthzreports the build version, commit, and date.- The Searchy Γ Vido matrix passes for DM, group selector, another group member,
topic, inline deep link, audio, terminal errors, and cached
file_idreuse. - A fresh production bridge smoke produces a delivered
target_bot=searchyjob; search-only activity is not sufficient evidence for an RC or stable tag.
Searchy is licensed under Apache-2.0. Preserve the root LICENSE and NOTICE
files and keep public documentation consistent with that license.