Skip to main content

Deploy full-featured web apps in minutes. Auth, payments, admin, email, database — one import.

Project description

Tidepool

    ┌─────────────────────────────────────────────┐
    │  build locally  →  deploy in 30s  →  live   │
    │       ↑                                ↓    │
    │    iterate        auth · payments · email    │
    │       ↑              db · files · http       │
    │       └──────────  <you>.tidepool.sh  ──┘   │
    └─────────────────────────────────────────────┘

Build and deploy full-featured web apps in minutes. Auth, payments, admin, email, file storage, database, background tasks — all from a single tp object.

Designed for AI agents and humans with CLI coding tools like Claude Code. The API is self-documenting (curl https://tidepool.sh/api), and every feature is available first and foremost via the command line.

Tools such as auth flows, Stripe payments, admin and database ORM, multi-page routing, file storage, email, a key-value database, background tasks, etc are all built and documented with AI agents in mind. These tools are available via a single tp object; they are meant to provide abstractions for infrastructure that is hard for an AI to set up (eg, third party subscriptions that take lots of clicks/config). We abstract these away so that the AI can move fast in terms of core site design, logic, and styling. Logging and API errors/warnings/tips are meant to be as detailed as possible so that an agent, on its own, can quickly understand and solve whatever problems come up.

Pods scale horizontally to 10+ replicas with shared Postgres/Redis/R2 — enough for a Substack clone at 20k–50k DAU or a Reddit-style site at 5k–15k DAU. The goal: a fairly advanced website up and running in an afternoon. No servers, no Docker, no AWS.

Quickstart

Install

pip install tidepool

Init a pod

tidepool init my-blog
cd my-blog

Creates a directory with a default main.py:

import tp
tp.page('/', '<h1>Hello from Tidepool!</h1>')

Develop locally

tidepool dev
# Pod runs at http://localhost:8000

The dev server replicates production pod behavior: file I/O goes to ./tp_data/files/, tp.db persists to a JSON file, tp.state is readable at ?format=json. Stripe, R2, and email are optional — the app runs without them.

Deploy

tidepool --url https://tidepool.sh register --email you@example.com
# verify email, then:
tidepool deploy
# Pod is live at https://my-blog.tidepool.sh in ~30 seconds

deploy auto-discovers source files and uploads tp_data/files/ (images, media) and tp_data/secrets.json automatically.

Push & Pull

Pull a live pod to develop locally — all state comes with it:

tidepool pull abc123
# Creates my-blog/ with source files + tp_data/ (db, secrets, files)
cd my-blog
tidepool dev
# Edit code, add data, test locally...
tidepool push              # pushes everything back (hash remembered from pull)

pull downloads source files, tp.dbtp_data/db.json, tp.secretstp_data/secrets.json, and pod files → tp_data/files/. The dev server reads all of these natively — no conversion needed.

push auto-discovers source files (same as deploy) and uploads them along with tp_data/db.json (merge by default), tp_data/secrets.json, and all files in tp_data/files/. Pushing source files triggers a pod restart. Use -y to skip the confirmation prompt.

tidepool pull abc123 --dir .   # pull into current directory instead of a subdirectory
tidepool push abc123           # explicit hash (overrides remembered hash)
tidepool push --file main.py   # push specific files instead of auto-discover
tidepool push --secret STRIPE_KEY=sk_xxx               # override a secret
tidepool push --replace-db     # replace all db keys instead of merging
tidepool push --sync           # delete remote files not present locally
tidepool push -y               # skip confirmation prompt

.tpignore

Create a .tpignore file to exclude files from deploy and push (same syntax as .gitignore):

# Directories (trailing slash)
data/
notebooks/

# File patterns
*.csv
*.npy
*.pkl
*.parquet
pipeline.py
build_log.*

Always ignored regardless of .tpignore: tp_data/, __pycache__/, .git/, venv/, .venv/, node_modules/, dotfiles, and build_log.*. Files over 50MB are skipped automatically with a warning.

tidepool init generates a starter .tpignore. For existing projects, create one before your first deploy or push to avoid uploading data files, models, or build artifacts.

Eject Mode

For full control over the runtime, eject the internals into your project:

tidepool eject
# Copies tp_runtime.py, tp_server.py, tp_backend.py, tp_templates/ into your project

These files are now yours to modify. tidepool dev, deploy, and push auto-detect eject mode when tp_server.py exists in the project directory — no flags needed. To undo, delete the ejected files.

Runtime Tools

Use import tp at the top of every .py file. main.py runs once at startup to configure the pod — set auth, payments, seed data, register routes. The server dispatches requests directly to handlers.

Name Description Usage
tp.route Register a request handler with path params @tp.route('/post/:slug', methods=['GET'])
tp.page Register a static HTML page (no handler) tp.page('/about', '<h1>About</h1>')
tp.auth Full auth system. Presets: 'paywall' (pay-first + magic link) or 'standard' (email/password) tp.auth = 'paywall' or tp.auth = 'standard'
tp.payments Stripe subscriptions and one-time purchases (in cents) tp.payments = {products: [{id: 'pro', price: 500, recurring: 'month'}]}
tp.admin Auto-generated admin panel at /_admin/ tp.admin = {users: ['admin@example.com']}
tp.create_user Create user with hashed password (idempotent) tp.create_user('sam@x.com', 'pass', subscriptions={'pro': True})
tp.db Key-value store, 1GB limit, persisted across runs tp.db.set('post:slug', {...}) / tp.db.get('post:slug')
tp.files File storage (R2 in prod, 50GB), served at /_files/ tp.files.write('photo.jpg', data) / tp.files.read('photo.jpg')
tp.email Send email with optional HTML and attachments tp.email('user@x.com', 'Subject', 'body', html='<p>hi</p>')
tp.http HTTP client (same API as requests), 200 req/60s, SSRF-protected tp.http.post(url, json=payload, headers={...})
tp.markdown Convert markdown to HTML (tables, code, footnotes) html = tp.markdown('# Hello\n\nWorld')
tp.secrets Read-only dict of deploy-time credentials api_key = tp.secrets['STRIPE_KEY']
tp.state Public app state dict, readable at ?format=json tp.state = {'status': 'live'}
tp.publish Update public JSON state (ETag-supported polling) tp.publish({'messages': msgs})
tp.background Background tasks (max 5). seconds<=0: once, >0: loop @tp.background(seconds=3600)

Handler return values: str → 200 HTML, dict/list → 200 JSON, int → status code, tuple(body, status) → body + status, None → 303 redirect, generator → SSE stream.

Request object: Handler receives (req, **params). Attributes: req.path, req.method, req.query, req.user (dict or None), req.body (dict), req.files (dict of upload metadata — file data auto-saved to tp.files).

Auth details

Auth presets: tp.auth = 'paywall' for payment-first apps (no signup form, accounts auto-created at checkout, magic link for return logins — pair with tp.payments). tp.auth = 'standard' for traditional email/password signup with confirmation, reset, and magic link. Customize after setting a preset: tp.auth['required'] = ['/dash/*'], tp.auth['theme'] = {'accent': '#e74c3c'}. Or pass a full dict for manual control: tp.auth = {required: ['/dash/*'], signup: True, reset: True, oauth: ['google']}.

Email confirmation on by default — signup_confirm: False to disable. req.user in handlers gives the logged-in user including subscriptions, purchases, and avatar_url (from Google profile). Theme: theme: {page: '<html>...{content}...</html>'} wraps auth pages in your layout; {content} receives the form, {title} the page title. Simpler: theme: {accent: '#color', css: '...'}.

Google OAuth setup: Add google_client_id and google_client_secret to tp_data/secrets.json. Get credentials at Google Cloud Console → Create OAuth 2.0 Client ID (Web application). Add authorized redirect URI: http://localhost:8000/_auth/oauth/google/callback for dev, https://yourdomain.com/_auth/oauth/google/callback for prod. That's it — the server handles the rest.

Payments details

Set tp.payments = {products: [{id: 'pro', name: 'Pro', price: 500, recurring: 'month'}]}. Users pay at /_pay/pro, manage subscriptions at /_pay/portal. recurring: 'month'/'year' for subscriptions; omit for one-time. Dev mode simulates purchases instantly. Prod requires tidepool stripe-connect (one-time setup).

Admin details

Set tp.admin = {models: {post: {fields: {title: 'string', body: 'text', tier: 'choice:free,pro'}, display: ['title']}}}. Field types: string, text, bool, number, date, choice:a,b,c. Plus read-only views of users, payments, emails, files. If not set, auto-inferred from tp.db key patterns. Admin access control: with tp.auth configured, set users: ['admin@example.com'] to restrict to specific emails (otherwise any logged-in user can access). Without tp.auth, admin is open in dev and key-gated in prod (key printed in server logs at startup, access via /_admin?key=<key>).

Background tasks

@tp.background()  # runs once at startup
def migrate(tp):
    if not tp.db.get('_migrated_v2'):
        for key, val in tp.db.prefix('post:'):
            val['version'] = 2
            tp.db.set(key, val)
        tp.db.set('_migrated_v2', True)

@tp.background(seconds=3600)  # every hour
def send_digest(tp):
    for email, user in tp.users().items():
        if user.get('subscriptions', {}).get('digest'):
            posts = tp.db.prefix('post:', reverse=True, limit=5)
            tp.email(email, 'Hourly Digest', '\n'.join(t for _, t in posts))

Server-Sent Events (SSE)

Return a generator from any route handler to stream real-time events:

@tp.route('/feed/live')
def live_feed(req):
    def stream():
        last_count = 0
        while True:
            messages = tp.db.prefix('msg:', reverse=True, limit=20)
            if len(messages) != last_count:
                last_count = len(messages)
                yield {'messages': [m for _, m in messages]}
            time.sleep(2)
    return stream()

Client-side: new EventSource('/feed/live'). Max 100 concurrent SSE connections per pod.

Static files & templating

  • Static files — Files in static/ alongside main.py are served at /static/<path>.
  • Jinja2 — Pre-installed. from jinja2 import Environment, FileSystemLoader; env = Environment(loader=FileSystemLoader('templates'), autoescape=True). Render: env.get_template('page.html').render(posts=p).

Full API reference

curl https://tidepool.sh/api

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

tidepool-0.1.3.tar.gz (54.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

tidepool-0.1.3-py3-none-any.whl (52.1 kB view details)

Uploaded Python 3

File details

Details for the file tidepool-0.1.3.tar.gz.

File metadata

  • Download URL: tidepool-0.1.3.tar.gz
  • Upload date:
  • Size: 54.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.3

File hashes

Hashes for tidepool-0.1.3.tar.gz
Algorithm Hash digest
SHA256 6ef7058dae097cb146d0a2787f81968f485fbb89cd4fd3f5ad4181eef9f37bb6
MD5 f7540821bf43607f0aa7bc10cc4e8818
BLAKE2b-256 602ffc823b6d90878a80c9ed83139827a3134f092782cecb4d2a28e76bb0af25

See more details on using hashes here.

File details

Details for the file tidepool-0.1.3-py3-none-any.whl.

File metadata

  • Download URL: tidepool-0.1.3-py3-none-any.whl
  • Upload date:
  • Size: 52.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.3

File hashes

Hashes for tidepool-0.1.3-py3-none-any.whl
Algorithm Hash digest
SHA256 091817e7b46b1fdf8763be9964e92ccbe0eca7ce8110996089307263460ce74c
MD5 0926a1efa436995567641d9fdf50411b
BLAKE2b-256 141dbd22ad4b8b41e041639bd2a293dd00669b07d0cd84e3120ce60f12f697cd

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page