AWS Lambda container and local developer runner for synchronizing Notion task databases with Google Calendar events.
This tool mutates both Notion and Google Calendar data. Validate configuration and run against test data first.
Releases: https://github.com/HUIXIN-TW/NotionSyncGCal/releases
- Bidirectional sync between Notion tasks and Google Calendar events.
- Supports multiple Google calendars via
gcal_dic. - Supports custom Notion property mapping via
page_property. - Supports force-sync modes via CLI (
-g,-n). - Handles cancelled Google Calendar events during sync filtering.
- Expands recurring Google Calendar events and uses each expanded event instance ID for matching/sync.
- Persists cloud sync logs in DynamoDB.
- A Notion task without a linked GCal event ID creates a Google Calendar event.
- An unmatched Google Calendar event creates a Notion task.
- Matched Notion/GCal records are updated based on last-modified timestamps.
- A Notion deletion flag deletes the linked Google Calendar event and the Notion task.
- CLI date flags are runtime in-memory overrides only and do not rewrite local JSON config.
The runtime uses an explicit mode switch via APP_MODE:
APP_MODE=local: uses.env.localsecrets andconfig/local.notion-setting.jsonfor local development.APP_MODE=cloud: uses DynamoDB records keyed byuuidand resolves cloud secrets from SSM SecureString paths at runtime.
Current cloud/runtime notes:
- Cloud secret values are resolved via:
GOOGLE_CALENDAR_CLIENT_SECRET_SSM_PATHTOKEN_ENCRYPTION_KEY_SSM_PATH
- Cloud runtime should not use plaintext
GOOGLE_CALENDAR_CLIENT_SECRETor plaintextTOKEN_ENCRYPTION_KEYenv vars. - Token JSON files under
token/are not runtime inputs. - Cloud token payloads at rest in DynamoDB should remain
enc:v1:encrypted.
- Python
>=3.11(frompyproject.toml) uv- Notion account + Notion integration token
- Google account + OAuth client credentials
- AWS account only for
APP_MODE=cloud
Install dependencies:
uv syncRun tests:
uv run python -m unittest discover -s test -vRun coverage:
uv run coverage run -m unittest discover -s test -v
uv run coverage report -mCoverage enforcement is configured in .coveragerc (fail_under = 50).
No AWS dependency for runtime.
- Secrets are read from
.env.local:NOTION_TOKENGOOGLE_CLIENT_IDGOOGLE_CLIENT_SECRETGOOGLE_REFRESH_TOKENTOKEN_ENCRYPTION_KEYonly when local token values are stored asenc:v1:payloads
- Structured local sync config is read from:
config/local.notion-setting.json
Requires a uuid and AWS access.
- Loads user config and tokens from DynamoDB tables (UUID-keyed):
- user config table
- Google OAuth token table
- Notion OAuth token table
- sync logs table
- Lambda environment includes SSM parameter paths:
GOOGLE_CALENDAR_CLIENT_SECRET_SSM_PATHTOKEN_ENCRYPTION_KEY_SSM_PATH
- Runtime resolves SSM SecureString values with decryption.
- Runtime does not use plaintext
GOOGLE_CALENDAR_CLIENT_SECRETor plaintextTOKEN_ENCRYPTION_KEYenv vars. - Runtime does not use local
token/*.jsonfiles.
Create local files from examples:
cp .env.local.example .env.local
cp config/local.notion-setting.example.json config/local.notion-setting.jsonRun sync locally with explicit mode:
APP_MODE=local uv run python src/main.py
APP_MODE=local uv run python src/main.py -t <goback_days> <goforward_days>
APP_MODE=local uv run python src/main.py -g <goback_days> <goforward_days>
APP_MODE=local uv run python src/main.py -n <goback_days> <goforward_days>CLI date range flags (-t, -g, -n) are runtime in-memory overrides only. They do not modify config/local.notion-setting.json.
Generate a local GOOGLE_REFRESH_TOKEN with:
uv run python scripts/generate-google-refresh-token.py --client-id <client_id> --client-secret <client_secret>Do not commit .env.local.
Required Lambda environment shape:
APP_MODE=cloud
APP_STAGE=dev
APP_REGION=ap-southeast-2
DYNAMODB_USER_TABLE=...
DYNAMODB_SYNC_LOGS_TABLE=...
DYNAMODB_GOOGLE_OAUTH_TOKEN_TABLE=...
DYNAMODB_NOTION_OAUTH_TOKEN_TABLE=...
GOOGLE_CALENDAR_CLIENT_ID=...
GOOGLE_CALENDAR_CLIENT_SECRET_SSM_PATH=/dev/notica/google_calendar_client_secret
TOKEN_ENCRYPTION_KEY_SSM_PATH=/dev/notica/token_encryption_keyIAM for Lambda execution role should include least privilege:
- DynamoDB read/write permissions for exact tables and required indexes.
- SSM permissions:
ssm:GetParameterfor runtime single-parameter secret resolution.ssm:GetParametersonly if batch secret lookup is introduced.- Permissions must be scoped to exact parameter ARNs.
kms:Decryptonly if those SecureString parameters use a customer-managed KMS key.
Avoid wildcard permissions such as ssm:*.
Detailed deployment workflow behavior is documented in docs/deployment.md.
Run local code with dev cloud configuration:
./scripts/local-run-dev-sync.sh --mode cloud --uuid <uuid>Run local-only mode:
./scripts/local-run-dev-sync.sh --mode localNotes:
- Cloud runner loads dev Lambda env configuration and resolves SSM values using your current AWS credentials.
- Local runner reads
.env.local. - Runner output is designed not to print sensitive secret values.
.
├── .coveragerc
├── .env.local.example
├── config/
│ └── local.notion-setting.example.json
├── docs/
│ ├── deployment.md
│ └── local-dev-sync-runner.md
├── lambda_function.py
├── pyproject.toml
├── scripts/
│ ├── generate-google-refresh-token.py
│ ├── local-run-dev-sync.sh
│ └── local_invoke_sync_lambda.py
├── src/
│ ├── config/config.py
│ ├── gcal/
│ ├── notion/
│ ├── sync/sync.py
│ └── utils/
│ ├── ssm_secrets.py
│ └── token_crypto.py
└── test/
- Local secrets (
.env.local) are gitignored. config/local.notion-setting.jsonis gitignored.token/is deprecated and ignored.- Cloud secret inputs are SSM path env vars, not plaintext secret env values.
- Cloud token payloads in DynamoDB should stay
enc:v1:encrypted at rest. - Do not log tokens or secret values.
- Dev deploy:
.github/workflows/deploy-dev-lambda.yml- Trigger: push to
dev - Runs validation (format/lint/unit tests/coverage/secret checks/workflow guardrails) before deploy
- Builds and pushes image, then updates dev Lambda
- Trigger: push to
- Release:
.github/workflows/release-semantic.yml- Trigger: push to
master - Runs validation before semantic release
- Creates Git tag and GitHub Release only
- Trigger: push to
- Production Lambda deploy workflow exists under
.github/workflows/disabled/and is currently disabled/manual.
docs/local-dev-sync-runner.md: detailed local/cloud runner behavior and troubleshootingdocs/deployment.md: CI/CD and environment-level deployment policy